ADR-014: No Docker — systemd units behind Caddy, deployed with git archive
Who is this for
Section titled “Who is this for”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
HEADcopied 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.
Context
Section titled “Context”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.
Decision
Section titled “Decision”- Processes.
sticker-apiruns uvicorn bound to the loopback interface on port 8000.sticker-botrunsmain.pyand must be a single instance. Both readbot/.envthroughEnvironmentFile, restart automatically, and are sandboxed (NoNewPrivileges, read-only code tree, write access only tobot/dataandlogs) (deploy/systemd/). - Limits. Each unit has
MemoryHighandMemoryMaxand 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
importof asites/directory; each project owns one file there.deploy/caddy.shinstalls only this project’s file, validates before reloading and rolls its own change back if validation fails. - Deploy.
deploy/push.ps1archives the committed tree, checks the archive for files that must never ship, uploads it, writes aVERSIONfile and installs pinned dependencies.deploy/update.shrestarts the services, reinstalls changed unit files and checks/health. - Dependencies are pinned for the server’s Python in
bot/requirements.txt; ranges live inrequirements.in. - Landing page is a separate repository deployed by Vercel on every push.
Alternatives considered
Section titled “Alternatives considered”| 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). |
Consequences
Section titled “Consequences”- 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.
Evidence
Section titled “Evidence”deploy/README.md;tests/test_deploy.py,tests/test_deploy_caddy.py.- Commits
f7da08c,7c5c574,dc59341.
Where to go next
Section titled “Where to go next”Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.