Skip to content

ADR-014: No Docker — systemd units behind Caddy, deployed with git archive

Developers and operators — read this before introducing containers, CI deploys or a second server.

  • Two systemd units (sticker-api, sticker-bot) run from a virtualenv under an unprivileged user; Caddy terminates TLS and proxies to the API on the loopback interface.
  • Deploy = archive of HEAD copied over SSH, then a restart script. Only committed code can be deployed.
  • The droplet is shared with another project; Caddy sites and resource limits are arranged so neither breaks the other.
  • Status: accepted. Docker is not used anywhere in the project, locally or on the server.

The team is small, development happens on Windows, and the pilot needed one small server. The tablet’s camera works only over HTTPS, so a real certificate was required from the first on-site test. Later a second project moved onto the same droplet.

  • Processes. sticker-api runs uvicorn bound to the loopback interface on port 8000. sticker-bot runs main.py and must be a single instance. Both read bot/.env through EnvironmentFile, restart automatically, and are sandboxed (NoNewPrivileges, read-only code tree, write access only to bot/data and logs) (deploy/systemd/).
  • Limits. Each unit has MemoryHigh and MemoryMax and a lower CPU weight than the neighbour, sized from the measured peak of a pack.
  • TLS. Caddy obtains certificates automatically. The main Caddyfile is a single import of a sites/ directory; each project owns one file there. deploy/caddy.sh installs only this project’s file, validates before reloading and rolls its own change back if validation fails.
  • Deploy. deploy/push.ps1 archives the committed tree, checks the archive for files that must never ship, uploads it, writes a VERSION file and installs pinned dependencies. deploy/update.sh restarts the services, reinstalls changed unit files and checks /health.
  • Dependencies are pinned for the server’s Python in bot/requirements.txt; ranges live in requirements.in.
  • Landing page is a separate repository deployed by Vercel on every push.
Alternative Why not
Docker / Compose A project rule: nothing to gain for two Python processes, and one more layer to debug on event day.
A platform-as-a-service for the backend The worker needs local disk, about 1.4 GB of memory per pack and long-running loops.
CI-driven deploys Deploys are rare and deliberately manual around events (code freeze).
  • Deploy needs a Windows machine with SSH access, or the equivalent manual steps.
  • Files deleted from the repository stay on the server after a push (harmless, but worth knowing).
  • A mistake in the neighbour’s Caddy file combined with a Caddy restart takes this product’s HTTPS down too — see Runbook: kiosk 502.
  • Line endings matter: the repository forces LF, and the push script refuses deploy files with CR.
  • Horizontal scaling is not available without rework — see Scaling scenarios.

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.