Two jobs, very different sizes:
| Situation | Path | Time |
|---|---|---|
| New site/app on a server that already exists | Fast path below | ~10–30 min |
| Brand-new server (fresh box, a client) | references/new-server.md |
~2 hours |
If the user has a filled-in references/server-profile.md (copied from
server-profile.template.md), read it first — real hostnames, paths and ports
instead of guesses. Never write the user's real IPs, tokens or secrets into files that
get committed or published.
Verify rather than assume: dig, ss -tlnp, systemctl, nginx -t before acting.
Cloudflare's proxy carries HTTP/HTTPS only, so ssh user@domain stops working
once a domain is proxied. Use the IP through an SSH alias (ssh <server>).
Label every command you give the user with where it runs — laptop or server.
A <sub> → <server-ip>. Proxied is correct immediately when a wildcard origin
certificate covers *.<base-domain>. Grey cloud only when the origin has no cert yet
(proxying then gives 5xx everywhere). Check: dig +short <domain> @1.1.1.1.
/var/www/<domain>/, root-owned, 644.127.0.0.1:<app-port> only; pick a free port with
sudo ss -tlnp. Code + .env (600) in /home/<deploy-user>/apps/<app>.
Every API response sends Cache-Control: no-store; /api/health returns 200.sudo scripts/add-site.sh <domain> --proxy <app-port> or --static <web-root> —
writes the block, runs nginx -t, reloads only on success. Then include the shared
snippets and caching rules from references/nginx-blocks.md. Always
sudo nginx -t && sudo systemctl reload nginx.
scripts/app.service.template → /etc/systemd/system/<app>.service:
node --max-old-space-size=128 dist/index.js, MemoryMax=256M, Restart=always.
Never tsx, ts-node, nodemon, a dev server or npm start in production. Prove
it: pkill -9 -f "[d]ist/index.js" → new PID; reboot → comes back alone. Details:
references/systemd-units.md.
scripts/deploy.sh.template → the app repo's scripts/deploy.sh: tests → build on
the laptop → back up DB → rsync → npm ci --omit=dev → migrate → publish web via a
staging dir → restart → health check → check from outside → print memory. Backup
comes before migrating; no auto-rollback (print the rollback command instead);
fail closed if any build fails.
curl -s -o /dev/null -L -w "%{http_code} tls=%{ssl_verify_result} http/%{http_version}\n" https://<domain>/
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" http://<domain>/
curl -sI https://<domain>/index.html | grep -ciE 'x-content-type|x-frame|referrer|strict-transport'
Want 200 tls=0 http/2, 301, and 4.
scripts/backup-db.sh <app> nightly from cron, scripts/restore-db.sh <app> drill
once now and monthly, scripts/pull-backups.sh on the laptop via launchd (not macOS
cron). Measure memory after the deploy and again a day later.
Work outwards: service worker → browser → Cloudflare edge → origin.
curl -sI https://<domain>/file | grep -i cf-cache-status rules out two layers.
Compare with the origin directly:
curl -sI -k --resolve <domain>:443:<server-ip> https://<domain>/file.
DNS looks wrong but dig is right → OS and browser caches, not the server.
For any error text, search references/gotchas.md first.
State recurring costs as per month, until removed. Snapshots bill monthly per
snapshot forever — take one only right before a risky change and delete it once
proven. Prefer the provider's flat-price automated backups. Before upgrading a server,
measure (free -m, per-service MemoryCurrent); if resizing, CPU/RAM only — disk
resizes are permanent.
| File | Use it when |
|---|---|
references/server-profile.template.md |
copy to server-profile.md and fill in once per server (keep it private) |
references/new-server.md |
building a server from scratch |
references/nginx-blocks.md |
writing server blocks, headers, caching, PWA, real-IP |
references/systemd-units.md |
turning an app into a capped service |
references/gotchas.md |
something is broken, or before touching SSH/Postgres/TLS/caching |
scripts/add-site.sh |
server: create + test + reload an Nginx site |
scripts/app.service.template |
the systemd unit |
scripts/deploy.sh.template |
the app repo's one-command deploy |
scripts/backup-db.sh / restore-db.sh |
server: nightly dump / restore drill / real recovery |
scripts/pull-backups.sh + .plist.template |
laptop: off-machine copy via launchd |