Ship Data Apps with AI

๐Ÿš€ Ship it liveยทLesson 11 of 26ยท~25 minยทBeginner

๐Ÿ–ฅ๏ธ Your own server

Rent one small VPS, log in with a key, learn Docker in ten minutes, and let Claude manage DNS through the Hostinger MCP without wiping your zone.

You'll leave with

  • A VPS you can SSH into with a key
  • The Hostinger MCP connected, so DNS changes happen by asking
  • A subdomain pointing at your server, checked from the outside

Your app works on your laptop, and nobody else can open it. Today it gets a home: a small rented computer that never sleeps, a key to log in with instead of a password, and a connection that lets Claude change your domain's DNS records (its entries in the internet's address book) when you ask. It goes live next lesson. Bring a laptop or desktop: a phone can't make the key.

โš ๏ธ No domain yet? A domain is your name on the web, like yourdomain.com, rented by the year. Buy one now (step 5 says where): a brand-new domain, or one moving its DNS, can take hours to start working.

1

Rent one small computer

Everything in this course runs on one VPS from Hostinger, a hosting company: a Linux computer in a data center, always on, under your control.

What's a VPS (and Linux, Ubuntu and root)?

A virtual private server: your own slice of a big computer in a data center (a building full of servers), rented by the month. It runs Linux, the free operating system most servers use, in a popular version called Ubuntu. root is its all-powerful admin user, the one you'll log in as.

๐Ÿ“ My whole fleet shares one box with about 4 GB of memory. When I added my medical price tool, healthcare.traqqit.com, it joined that box without touching the apps already running there.

On hostinger.com, open VPS hosting and choose:

  • Plan: KVM 1, with one vCPU (processor core) and 4 GB of memory: the size the whole fleet runs on.
  • Billing: a long prepay costs a few dollars a month; renewal costs more, so check both prices.
  • Data center: the one nearest your users.
  • OS template (the system it starts with): Ubuntu 24.04 with Docker, the tool that runs apps in sealed boxes (step 3).
  • SSH key: make yours first (step 2's first two commands) and paste in its public half.
  • Root password: long and random, kept in a password manager (the app or browser feature that stores passwords).
What are SSH and an SSH key?

SSH lets you type commands on another computer from your own terminal (the window where you type commands), over an encrypted connection. A key is a pair of files made on your laptop: a public half you can hand to anyone, and a private half that never leaves your laptop and proves it's you.

When setup finishes, open hPanel (Hostinger's control panel), click VPS in the left menu, then your server. The overview shows it running, with an IP address: four numbers with dots, like 203.0.113.7 (reserved for examples). That's your server's address on the internet, written YOUR_VPS_IP from here on; type yours wherever you see it.

Then note the server in NOTES.md, the file Claude Code (the AI assistant in VS Code, your code editor) reads each session, from Work with Claude Code:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouAdd a Server section to NOTES.md: Hostinger KVM 1, Ubuntu 24.04 with Docker, IP YOUR_VPS_IP, root login by SSH key. No passwords or tokens.
ClaudeAdded, with no secrets in it. Open NOTES.md to check.โ†ณ Edit ยท NOTES.md
2

Log in with a key, not a password

In VS Code, open a terminal (View โ†’ Terminal; PowerShell, Git Bash or the Mac terminal) and make the key:

ssh-keygen -t ed25519

Press Enter to accept the suggested file. If it asks Overwrite (y/n)?, you already have a key: type n and use it. It then asks twice for a passphrase (a password on the key file): press Enter both times to leave it empty. A small square of symbols means the key was made.

Why empty: Claude can't type a passphrase, so guard the laptop instead, with a login password and disk encryption (BitLocker or Device Encryption on Windows, FileVault on a Mac). Want one anyway? Load the key into ssh-agent (a helper that holds it unlocked) yourself first; Claude can explain how. Now show the public half:

cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAโ€ฆ you@your-laptop

Copy that whole line (select it, then Ctrl+C in VS Code's terminal; Mac: Cmd+C) into the SSH key box during setup, or later in hPanel at VPS โ†’ your server โ†’ Settings โ†’ SSH keys. The file without .pub is the private half: it never goes to Hostinger, the server or a chat.

Now log in yourself, once:

ssh root@YOUR_VPS_IP

Replace YOUR_VPS_IP with your server's address. At Are you sure you want to continue connecting (yes/no/[fingerprint])? type yes: your laptop now remembers the server's fingerprint (a code that identifies it), so Claude's ssh commands won't hang on that question. You should see:

Welcome to Ubuntu 24.04 โ€ฆ root@โ€ฆ:~#

A prompt ending in # means you're on the server. Blocks labeled On the server go there; Your computer means your laptop. Keep a terminal tab for each (the + in VS Code's terminal panel), and type exit to leave. Asked for a password instead? The server lacks your public key: add it in hPanel.

Send commands in single quotes

Claude runs server commands through ssh: ssh root@YOUR_VPS_IP 'docker ps'. Use single quotes: inside double quotes, your laptop's shell (your terminal's command language) runs backticks and $( ), two ways of writing "run this and paste in the result", before the server sees them. Try it:

ssh root@YOUR_VPS_IP 'echo $(hostname)'
ssh root@YOUR_VPS_IP "echo $(hostname)"

The first prints your server's name, the second your laptop's. While I worked on The List, double quotes silently ate a word from a settings-file comment, twice in one session.

Close the password door

The server still accepts the root password from anyone who guesses it. Close that door, with your server tab still logged in as a safety line:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouTurn off password logins: write PasswordAuthentication no and KbdInteractiveAuthentication no into /etc/ssh/sshd_config.d/00-keys-only.conf, test the config, reload ssh, and show me the settings it really uses.
ClaudeThe test passed and ssh reloaded. Its live settings read passwordauthentication no and kbdinteractiveauthentication no. Keep your open session until a fresh login works.โ†ณ Bash ยท ssh root@YOUR_VPS_IP 'printf โ€ฆ > /etc/ssh/sshd_config.d/00-keys-only.conf'โ†ณ Bash ยท ssh root@YOUR_VPS_IP 'sshd -t && systemctl reload ssh && sshd -T | grep โ€ฆ'

What's happening underneath:

printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n' > /etc/ssh/sshd_config.d/00-keys-only.conf
sshd -t && systemctl reload ssh
sshd -T | grep -E '^(password|kbdinteractive)authentication '

sshd, the SSH program on the server, keeps the first value it reads, and 00- puts your file ahead of the usual provider files. The reload runs only if the test passes (&&) and leaves open sessions alone. The last line should print:

passwordauthentication no kbdinteractiveauthentication no

Check from outside: in a new laptop tab, ssh root@YOUR_VPS_IP still logs you straight in, and a forced password login is refused:

ssh -o PubkeyAuthentication=no root@YOUR_VPS_IP
root@YOUR_VPS_IP: Permission denied (publickey).

If it asks for a password, press Ctrl+C and ask Claude why. If the fresh login fails, undo it from the open session: rm /etc/ssh/sshd_config.d/00-keys-only.conf && systemctl reload ssh.

Open only three doors

What are ports and a firewall?

Ports are numbered doors on a server: 22 for SSH, 80 for plain http://, 443 for https://. A firewall decides which doors the internet may knock on. Hostinger's filters traffic before it reaches your server, so even a port opened by mistake later stays shut.

In hPanel, open VPS โ†’ your server โ†’ Security โ†’ Firewall (menus move; see Hostinger's help pages). Create a firewall with Accept rules from anywhere for TCP (ordinary traffic) ports 22, 80 and 443, plus UDP 443 for HTTP/3, the faster HTTPS the next lesson turns on. Add the rules before activating: an empty firewall blocks everything, SSH included. Rules apply within two minutes and leave open sessions alone, so wait, then test with a fresh ssh in a new tab; if that fails, deactivate the firewall on the same screen.

๐Ÿฆฅ Skipped: a separate admin user and extra hardening tools. With passwords off and three doors open, a one-person server can wait; give anyone who joins their own user and key.

3

Docker in ten minutes

What are Docker, an image and a container?

Docker runs each program in its own sealed box. An image is the recipe: a program plus everything it needs, downloaded once. A container is a box made from it, running. Delete the box and the recipe stays.

The traqqit.com home page is one small container made from caddy:2, the official image of the Caddy web server. Containers like it start with docker run and a few flags: -d (run in the background), --name, --restart unless-stopped (come back after a crash or a reboot, unless you stopped it) and --network (join a private network where containers find each other by name). docker ps (what's running), docker logs NAME (what it printed) and docker network cover most days.

The fleet's rule: all containers join one private network and publish no ports. Only the front door owns ports 80 and 443, and it reaches each app by container name.

What's the front door (a reverse proxy)?

One program that receives every web request (a browser asking for a page) and hands it to the right container by name. The fleet's is Caddy, which also gets the certificates behind the padlock. Yours arrives next lesson.

What does publishing a port mean?

Opening a container's door on the server's public address with -p, so anyone can reach it around the front door: how databases end up open to the world. Without -p, containers still reach each other privately.

Try it with a throwaway container:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouOn my server, run caddy:2 as hello on a new network called practice, no published ports. Show docker ps, fetch its page by name from a second container and from the server itself, explaining each command in one line, then remove both.
Claudehello was Up, no published ports. A second container got the page titled Caddy works!; the server couldn't resolve hello. Both removed; docker ps lists nothing.โ†ณ Bash ยท ssh root@YOUR_VPS_IP 'docker network create practice && docker run -d โ€ฆ'โ†ณ Bash ยท ssh root@YOUR_VPS_IP 'curl -sS http://hello/'

What's happening underneath, to run yourself in your server tab:

docker network create practice
docker run -d --name hello --restart unless-stopped --network practice caddy:2
docker ps

The first run downloads the image. In docker ps, hello says Up, and PORTS shows entries like 80/tcp with no arrow: doors inside the box, not published (a published one reads 0.0.0.0:8000->8000/tcp). Fetch its page by name from a second container (wget downloads), then from the server with curl (which fetches an address and shows the raw answer). grep -o keeps only the matching part:

docker run --rm --network practice caddy:2 wget -qO- http://hello/ | grep -o '<title>.*</title>'
curl -sS http://hello/
<title>Caddy works!</title> curl: (6) Could not resolve host: hello

Names only resolve inside their network. For a one-off check from the server, use the container's private address:

IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' hello) && curl -s http://$IP/ | grep -o '<title>.*</title>'

Same title; the $( ) ran on the server, where you typed it. Clean up (the image stays):

docker rm -f hello && docker network rm practice
docker ps

It prints hello and practice, then docker ps shows only its header line.

Plain docker run, plus a README

A side service doesn't need fancier tools: plain docker run is fine if a README (a notes file) beside it records the exact command, where its env file lives and its front-door block. An env file holds secret settings, one NAME=value per line, read by the program at startup. Nobody remembers the flags after a rebuild; my analytics service runs exactly this way. For example, with your own names:

# myservice
Run:      docker run -d --name myservice --restart unless-stopped --network web --env-file /root/secrets/myservice.env IMAGE:TAG
Env file: /root/secrets/myservice.env (the values live there, never here)
Front-door block:
  myservice.yourdomain.com {
      reverse_proxy myservice:3000
  }

web and /root/secrets are the shared network and root-only secrets folder from Deploy behind one front door. Forgot a container's flags? docker inspect NAME shows them, environment values included, so never paste its output into a chat.

4

Connect Claude to Hostinger

What's an MCP server?

A plug-in that gives Claude Code real tools (MCP: Model Context Protocol). Hostinger's turns its API, the addresses programs use to read and change your account, into tools for your VPS, domains and DNS.

Connect it once, from a plain terminal tab (not the Claude Code box):

claude mcp add --transport http --scope user hostinger https://mcp.hostinger.com

--scope user makes it work in every project folder. Then restart Claude Code (/exit, then claude), type /mcp, pick hostinger, choose Authenticate, and finish the Hostinger sign-in in the browser tab that opens. No token to copy. /mcp should now show hostinger as connected; if not, run the sign-in again from /mcp before planning around it. A plain terminal double-checks:

claude mcp list
hostinger: https://mcp.hostinger.com (HTTP) - โœ” Connected

๐Ÿ“ Acento's DNS record was made from inside a Claude Code session through this connector. In another session, the DNS tools never loaded, though the config file listed them. Configured isn't connected: check /mcp first.

Its first job is read-only:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouUsing the Hostinger MCP, list my VPS, its IP and my domain's DNS records. Don't change anything.
ClaudeOne VPS: KVM 1, Ubuntu 24.04 with Docker, running, IP YOUR_VPS_IP. Below are the records Hostinger made for yourdomain.com. Nothing was changed; compare both with hPanel.โ†ณ hostinger ยท search ยท find the VPS and DNS operationsโ†ณ hostinger ยท execute ยท list virtual machines, then DNS records

What's happening underneath: Claude uses Hostinger's search tool to find the right operation and its execute tool to run it. Read each execute request before approving, and never tick "don't ask again" for it: the same tool reads and changes. Keep this list for step 5.

๐Ÿ›‘ Only if you ever use an API token instead (a long password for programs): it controls your domains and billing. Hostinger's local MCP package reads it from an environment variable (a setting a program gets at startup), API_TOKEN. Never paste it into a chat or commit it to git (your project's saved history); if you do, revoke it in hPanel.

5

Your domain and DNS

Buy a domain before this lesson. The simplest place is the same Hostinger account: about $10โ€“20 for the first year (check the renewal price). Bought it elsewhere? Set its nameservers to the two hPanel shows for Hostinger DNS and allow up to 24 hours before any DNS step, or make records by hand at that company: Claude's Hostinger tools only reach DNS that Hostinger hosts.

What are nameservers, a zone, an A record and TTL?

Nameservers answer questions about your domain; whoever runs them is where you edit its records. The zone is that list of records. An A record is one line pointing a name at an IP address: myapp โ†’ YOUR_VPS_IP. TTL (time to live) is how many seconds other computers may remember the answer.

Each app gets one A record for its subdomain (a name under your domain, like myapp.yourdomain.com), with a TTL of 300 seconds, so a mistake heals in five minutes. A wildcard record (*) would send every subdomain to your server instead; the fleet uses one record per app. Use your app's short name for myapp and your domain for yourdomain.com.

๐Ÿ›‘ DNS update tools can replace records instead of adding yours: every record with that name, or in some tools the whole zone, and every other subdomain goes dark. So read first (step 4), add with overwrite set to false, and compare the full list before and after. Hostinger's tools can also list and restore zone snapshots (saved copies); ask Claude to show yours.

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouAdd an A record for myapp.yourdomain.com pointing to my VPS, TTL 300, overwrite false. Show me the record afterwards.
ClaudeAdded with overwrite false: myapp, type A, YOUR_VPS_IP, TTL 300. Your zone now equals the earlier list plus this one line. It may take a few minutes to show up elsewhere.โ†ณ hostinger ยท execute ยท update DNS records (overwrite: false), then list them

What's happening underneath: one line appended, everything else untouched; your domain's DNS page in hPanel shows the new myapp row too. To undo, ask Claude to delete that A record and list again. First Bites and Baby Signs got theirs through this same connector.

Saved isn't propagated

Saved means Hostinger has your record; propagated means the DNS servers everyone else asks can see it, which can take minutes.

๐Ÿ“ When I launched Acento, the DNS tool said the request was accepted, and the name didn't exist for more than three and a half minutes. My laptop gave unreliable answers that day, so the final check ran from the server.

Ask two public resolvers, big DNS servers anyone can query (Cloudflare's and Google's), what the internet sees. nslookup asks a DNS server what a name points at, in PowerShell or on a Mac:

nslookup myapp.yourdomain.com 1.1.1.1
nslookup myapp.yourdomain.com 8.8.8.8
Non-authoritative answer: Name: myapp.yourdomain.com Address: YOUR_VPS_IP

Both must print your IP; Non-existent domain or NXDOMAIN means not yet, so wait a few minutes. If your laptop disagrees with everyone else, or its lookup times out, ask from the server, over HTTPS:

curl -s "https://dns.google/resolve?name=myapp.yourdomain.com&type=A"
{"Status":0, โ€ฆ "Answer":[{"name":"myapp.yourdomain.com.","type":1,"TTL":300,"data":"YOUR_VPS_IP"}] โ€ฆ}

That's DNS over HTTPS: the same question as a web request on port 443, answered in JSON (data written as text). "Status":0 with your IP after "data" means live; "Status":3 means not yet.

โš ๏ธ Some servers block outgoing DNS on port 53, its usual door, so nslookup or dig (another lookup command) come back empty, exactly like "no such name". One of my scripts waited on a long-live record until it asked over HTTPS instead.

The bare domain (yourdomain.com, written @) may still show a parking page, a placeholder from your registrar (the company you bought it from). Leave it for now. Pointing @ at your server later is a replace, not an add: ask for that one record and compare the lists.

6

Check what answers on port 80

๐Ÿ“ When The List was brand new, first-time visitors who typed its name without https:// got a generic "Apache2 Default Page" with a 200 OK (the "it worked" code). Their browsers tried port 80 first, where a leftover Apache (an older web server) answered; my front door only listened on 443. Older sites hid the problem: browsers already used HTTPS there. A permanent redirect to https:// fixed it.

Before the next lesson hands ports 80 and 443 to your front door, see what holds them now:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouSSH in and tell me what's listening on ports 80 and 443.
ClaudeNothing on 80 or 443: ss shows SSH holding port 22. Run ss -ltnp on the server to see the same list.โ†ณ Bash ยท ssh root@YOUR_VPS_IP 'ss -ltnp'

What's happening underneath: ss lists the doors programs listen on, and who holds each.

ss -ltnp | grep -E ':(80|443) '

No output is what you want. A line naming apache2 or nginx (another web server) means something old holds the door. A fresh server doesn't need it: after your OK, Claude can run systemctl disable --now apache2 to stop it for good (systemctl enable --now apache2 undoes that). Your front door then owns port 80 and redirects to https:// itself.

Check from outside too (in PowerShell, type curl.exe instead of curl):

curl -sSI --max-time 10 http://myapp.yourdomain.com
You seeIt means
Failed to connectThe door is open and nobody's home yet: right until the next lesson, when this shows a redirect to https://
timed outThe firewall is dropping port 80: check its rules from step 2
200 OK with Server: ApacheA leftover web server owns port 80

When one visitor can't reach you

If one machine can't reach your site while you can, test from a second network before touching the server (your phone on mobile data, Wi-Fi off). On the failing machine, curl -v shows where it stops: the name lookup, the connection, or the HTTPS handshake (the opening exchange that sets up encryption).

๐Ÿ“ Once, a cloud machine running one of my AI agents couldn't reach my server: its own outbound filter opened the connection, then stalled the handshake. My server was fine all along.

Do it with Claude: the prompts

  1. Using the Hostinger MCP, list my VPS, its IP and my domain's DNS records. Don't change anything.
  2. Add an A record for myapp.yourdomain.com pointing to my VPS, TTL 300, overwrite false. Show me the record afterwards.
  3. SSH in and tell me what's listening on ports 80 and 443.

What's happening underneath: MCP exposes Hostinger's API as tools Claude can call, each waiting for your approval, and reading the records first is how you avoid wiping them later. Changes to your live DNS may stay blocked by Claude Code's own safety checks until you ask for them explicitly; then state the exact change yourself, or make it in hPanel.

Your turn

  • A fresh ssh root@YOUR_VPS_IP logs you in with no password question, and ssh -o PubkeyAuthentication=no root@YOUR_VPS_IP answers Permission denied (publickey).
  • hPanel shows your VPS firewall active, accepting ports 22, 80 and 443 only.
  • You saw Could not resolve host: hello on the server, cleaned up, and docker ps shows only its header line.
  • In a new Claude Code session, /mcp shows hostinger connected, and the read-only prompt lists the same IP and records as hPanel.
  • nslookup myapp.yourdomain.com 1.1.1.1 and the same with 8.8.8.8 both print YOUR_VPS_IP, and your zone differs from step 4's list by that one record.
  • On the server, ss -ltnp | grep -E ':(80|443) ' prints nothing, and NOTES.md holds your server's login line.

Next, Deploy behind one front door: your app moves onto this server, the front door takes ports 80 and 443, and myapp.yourdomain.com starts answering with a padlock, using the A record you made today.