Context
The site needs HTTPS on its own domain, a Next.js server, a Go API and Nginx in front. It must be reproducible and support rollback, without a recurring bill beyond one small server.
Options
- Kubernetes. Operational weight far beyond a portfolio's needs.
- A platform-as-a-service. Less control over Nginx and headers, and adds vendor coupling.
- One Linux VPS running Docker Compose, with Nginx as the only public listener.
Decision
CI builds the web, api and nginx images once per commit from one build graph, tests and scans them,
and, for commits on main, publishes exactly those images to GHCR tagged with the commit SHA. A deploy
resolves the tags to digests, records them with the release's own compose.yaml (shipped inside its nginx
image), replaces the containers, and verifies health through nginx. If any check fails, it restores the
previous release — the same digests and configuration — automatically. TLS certificates come from Let's Encrypt via certbot on the host, using
the webroot challenge. A deploy hook reloads Nginx after renewal. Application containers have no
published ports, run as non-root with read-only filesystems, and have memory and CPU limits.
Consequences
- Rollback is
deploy.sh rollback: the previous SHA's images are still on the host and in the registry. - A deploy can drop a few seconds of requests while a container is replaced. That is acceptable for this site, and Nginx retries an idempotent request once against the restarted upstream.
- One host is one failure domain. Recovering means re-running the bootstrap on a new host and redeploying. Documented in docs/operations.
- Since ADR-0010, CI and the deploy run on a self-hosted runner on this same host; the SSH deploy path was removed.
- Since ADR-0011, Cloudflare's CDN proxies public traffic to this host.