Deploying, and the machine it deploys to
The short version
Builds all four sites, uploads them, flips a symlink, reloads the web server,
and then checks over real HTTPS that every site and every feed returns 200. If
that last part fails it tells you, loudly.
To undo:
./deploy/rollback.sh # previous release
./deploy/rollback.sh --list # see what is available
Rolling back does not rebuild anything. It moves a symlink, so it takes about
as long as SSH takes to connect.
The server
|
|
| Host |
178.104.116.235 (IPv6 2a01:4f8:1c1a:9e3::1) |
| Provider |
ird.ro, tier 0x — 2 vCPU, 4 GB, 40 GB |
| OS |
Ubuntu 24.04, unattended security upgrades on |
| Firewall |
ufw: 22, 80, 443 only |
| SSH key |
~/.ssh/id_ed25519_eratic |
| Web server |
Caddy 2 in Docker, owns 80/443, automatic Let's Encrypt |
This is not the box the Agora API runs on. That was the original plan and
it was the wrong one: Caddy there already owned ports 80 and 443, so eratic
would have had to share a config file that Agora's own deploys overwrite. A
personal site and a production API also want different things from a server —
different reboot tolerance, different appetite for experiments. Separating them
costs about 36 credits a month and removes a whole category of problem.
Idle cost of the whole stack: about 18 MB of RAM.
Layout
/opt/web/
caddy/
docker-compose.yml
etc/ ← mounted at /etc/caddy in the container
Caddyfile server-wide options + import conf.d/*
conf.d/
00-placeholder.caddy keeps the import glob valid on an empty box
eratic.caddy this project (deployed from this repo)
sites/
eratic/
releases/<timestamp>-<sha>/{home,blog,fav,io}
current -> releases/<…> a RELATIVE symlink
media/ video and other large files
Two details that matter and are easy to get wrong:
current is a relative symlink. Caddy mounts /opt/web/sites and resolves
the link from inside the container, where /opt/web/... does not exist. An
absolute link produces a 404 that looks like a routing problem.
etc/ is mounted as a directory, never file-by-file. Docker pins a
single-file bind mount to that file's inode. rsync writes a temp file and
renames it, creating a new inode — so the container goes on serving the old
config while the host shows the new one, and the reload reports success. This
cost an hour; do not undo it.
Adding another domain later
The whole reason for the conf.d layout. For, say, cheri.ro:
- Point DNS at
178.104.116.235 (ird dns set cheri.ro @ A 178.104.116.235).
- Put the built files in
/opt/web/sites/cheri/current/.
- Drop a
cheri.caddy into /opt/web/caddy/etc/conf.d/.
docker exec web-caddy caddy reload --adapter caddyfile --config /etc/caddy/Caddyfile
Caddy fetches the certificate on the first request. Nothing about eratic
changes, and Caddy validates the whole set before loading any of it, so a
mistake in the new file cannot take the existing sites down.
What a deploy actually does
- Preflight — SSH reachable; notes uncommitted changes and continues.
- Build —
npm run build, which runs the GPS check on every source photo
first. A photograph with coordinates stops the deploy here.
- Upload —
rsync into releases/<new>/, hard-linking unchanged files
against the live release. Publishing one post transfers one post.
- Go live —
ln -sfn + mv -T, which replaces the symlink atomically.
There is no moment where current is missing or partial.
- Web server config —
deploy/eratic.caddy is uploaded and validated. If
Caddy rejects it, the file is removed and the running config is untouched.
- Housekeeping — keeps the last 5 releases.
- Verify — every site and every feed over HTTPS, with retries, because a
brand-new hostname takes a few seconds to get its certificate.
First-time setup
Only needed for a new server:
Installs Docker from Docker's signed APT repository, enables unattended
upgrades, closes the firewall to everything but 22/80/443, creates the layout,
and starts Caddy. Safe to re-run; it only changes what is missing.
Useful things to know
# Watch the web server
ssh -i ~/.ssh/id_ed25519_eratic root@178.104.116.235 \
'cd /opt/web/caddy && docker compose logs -f'
# What is live right now
ssh -i ~/.ssh/id_ed25519_eratic root@178.104.116.235 \
'readlink /opt/web/sites/eratic/current'
# Build without uploading anything
npm run deploy -- --dry-run
Every default can be overridden by environment variable: ERATIC_SERVER,
ERATIC_SSH_KEY, ERATIC_REMOTE_ROOT, ERATIC_PROJECT,
ERATIC_KEEP_RELEASES.
Backups
What is actually irreplaceable:
- The repository — every word and photograph. It is on GitHub and on your
laptop. That is two copies; a third somewhere offline would not hurt.
/opt/web/sites/eratic/media/ — video is not in git. This is the only
copy. See docs/05-media.md.
- The
caddy-data Docker volume — TLS certificates and the ACME account
key. Losing it is survivable (Caddy re-issues) but rate limits apply if it
happens repeatedly.
Everything else — the OS, Docker, the releases — is reproducible from
bootstrap.sh and deploy.sh in about ten minutes.