Interactive · Hosting

Hosting your first server.

You wrote an app and it runs on your laptop. Now you want strangers on the internet to use it. Here's the smallest end-to-end path from npm start on localhost to a real HTTPS URL. Five pieces. Roughly an afternoon.

1 Server 2 Your app 3 Process manager 4 Reverse proxy 5 Domain
01 / 05

A server.

Your laptop sleeps · wifi drops not a server You ssh client internet vm · ubuntu 22.04 · 1gb Rented box 203.0.113.42 always on datacenter ssh-only · port 22 ssh

Your laptop is not a server. Close it, lose wifi, and the app is gone. Rent a small computer that lives in a datacenter and never sleeps. $5–$10 a month gets you more than enough for a side project.

You'll pick a provider, click "create", and walk away with an IP address and an SSH key login. From there, everything happens over ssh from your laptop.

Cheap and simple

Hetzner Cloud, DigitalOcean, Vultr, Linode. Click a button, pay a few dollars a month, get a fresh Ubuntu box.

If you're already on a cloud

AWS Lightsail, GCP Compute Engine, Azure VM. Same shape, plumbed into the rest of your account.

your laptop ~/
# first connection, as root
ssh root@203.0.113.42
on the server root@vm
# patch the box, make a non-root user, turn on the firewall
apt update && apt upgrade -y
adduser deploy
usermod -aG sudo deploy
ufw allow OpenSSH
ufw enable
Next →

An empty box is not an app. You can SSH in. The box is online. Now you need your code on it.

02 / 05

Your app.

Browser you, testing http://<ip>:3000 vm · 203.0.113.42 3000 App node server.js listening :3000 foreground process tied to your ssh session HTTP

Get your code onto the box. The simplest path is git clone. Install the runtime, install dependencies, point at whatever it needs (env vars, a SQLite file, a connection string), and bind it to a port. 3000 is fine for now.

Open http://<your-ip>:3000 in your browser. If you see your app, it works. This is the moment your software is, technically, on the internet.

Getting code on the box

git clone from a public repo, or scp / rsync for private code. Later: a GitHub Actions deploy step.

Picking a port

Pick anything above 1024 that isn't taken. 3000 (Node), 8000 (Python), 8080 (Go) are conventional. You'll close the public-facing one in Phase 4.

on the server deploy@vm
# install the runtime and git
sudo apt install -y nodejs npm git

# pull your code, install deps, run it
git clone https://github.com/you/your-app.git
cd your-app
npm ci
npm run build
PORT=3000 npm start
open the port for testing root@vm
# let port 3000 through the firewall (temporarily)
sudo ufw allow 3000
Next →

Close the SSH session, the app dies. Reboot the box, nothing comes back up. Foreground processes don't survive anything. You need a process manager.

03 / 05

A process manager.

vm · 203.0.113.42 systemd unit · myapp.service auto-start at boot · restart on crash App node server.js · :3000 restart journald journalctl -u myapp -f structured · persistent stdout reboot → app comes back automatically

You need something between you and your app that keeps it alive: across crashes, across reboots, across deploys. systemd is already on every Ubuntu box. It's free, native, and good enough for almost every side project.

Write a tiny unit file, enable it, and forget about it. Logs go to journalctl, which is searchable, rotates for you, and survives reboots.

Default (Linux)

systemd. Already installed. No new dependencies. Same shape on every Linux box you'll ever rent.

Node-friendly

PM2. Clustering, log rotation, zero-downtime reloads. Nice if you want batteries included.

Python-friendly

Supervisor, or just write a systemd unit that runs uvicorn/gunicorn.

create the unit file /etc/systemd/system/myapp.service
[Unit]
Description=My app
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/your-app
ExecStart=/usr/bin/node /home/deploy/your-app/server.js
Environment=PORT=3000 NODE_ENV=production
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
turn it on root@vm
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

# follow logs in real time
journalctl -u myapp -f
Next →

Your app is alive, but it's bare HTTP on a weird port. No HTTPS, no real URL, anyone snooping the wifi at a coffee shop can read every login. Time for a reverse proxy.

Keep reading.

Drop your email to unlock the last two pieces.

04 / 05

A reverse proxy.

Internet users :80 · :443 vm · 203.0.113.42 nginx :80 · :443 tls termination gzip · static files proxy_pass → App 127.0.0.1:3000 localhost-only now (systemd-managed) https http

A reverse proxy is a tiny piece of software that sits between the internet and your app. It listens on the standard web ports (80 and 443), terminates TLS, and forwards the request to your app on localhost:3000.

Your app stops being on the public internet directly. It only listens on localhost. The proxy is the front door, and it's good at being a front door: it gzips responses, serves static files fast, and gives you one clean place to add rate limits, headers, and routing rules later.

The classic

Nginx. Boring in the best way. Every system admin has read your config before. Tons of documentation.

Newer, friendlier

Caddy. Smaller config, and it gets the HTTPS certificate for you automatically — no certbot step in Phase 5.

Container-native

Traefik. Best if you're running Docker or Kubernetes. Auto-discovers services from labels.

install nginx root@vm
sudo apt install -y nginx
write the site config /etc/nginx/sites-available/myapp
server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
enable and lock down root@vm
# turn the site on
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# open 80/443, close the direct app port
sudo ufw allow 'Nginx Full'
sudo ufw delete allow 3000
bind your app to localhost only server.js
// before: app.listen(3000)
app.listen(3000, '127.0.0.1');
Next →

People still type an IP address. Browsers still scream "Not Secure" because there's no certificate. One last piece.

Keep reading.

Drop your email to unlock the last piece.

05 / 05

A domain name.

User types in browser yourdomain.com DNS A record yourdomain.com → 203.0.113.42 registrar · cloudflare $10–15 / year vm nginx :443 https ✓ TLS cert let's encrypt auto-renews · 90 day lookup https final url https://yourdomain.com green lock · real name · done

An IP address is not a name people want to type. Buy a domain ($10–15 a year), and add an A record that points it at your server's IP. DNS propagation usually takes minutes, sometimes an hour.

Then get a free TLS certificate from Let's Encrypt. Certbot does the whole dance: it requests the cert, edits your nginx config to serve HTTPS, and sets up a cron job to renew it every 60 days. Visit your domain. Green lock. Done.

That's it. Your app is on the internet, on a name you own, over HTTPS, surviving reboots and crashes.

Registrars worth using

Cloudflare Registrar (at-cost, no markup). Porkbun. Namecheap. Avoid GoDaddy.

DNS hosting

Cloudflare DNS, free and fast. Some registrars host DNS for you too.

Certificates

Let's Encrypt via certbot, or Caddy (if you picked Caddy in Phase 4, you already have HTTPS).

at your registrar's DNS panel cloudflare · porkbun · namecheap
# add two records
yourdomain.com        A      203.0.113.42
www                   CNAME  yourdomain.com
get the TLS cert root@vm
# install certbot + the nginx plugin
sudo apt install -y certbot python3-certbot-nginx

# certbot reads your nginx config, gets a cert, rewrites the
# config to serve https, and installs a renewal cron
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
verify your browser
# open it. green lock. real URL.
https://yourdomain.com
What's next →

One box is one fuse. When the box dies, your site goes down. When you outgrow it, head over to System Evolution to see exactly what to scale next, and in what order.