Docker
CMSnap-LITE is a single fully static musl binary — FROM scratch works, no libc inside. A container needs the binary, a working directory and one volume for the state.
First run bootstraps everything
There is no separate container mode: the container simply runs /cms. Started with an empty working directory, the binary creates the root settings.json (bound to 0.0.0.0:8080 — reachable through the published port right away), this example site under www/default/, and prints the one-time first-run summary to stdout: the admin password, the /cms enrollment link (the admin zone is gate-hidden by default) and an MCP token. Read it with docker logs and open the enrollment link first.
Dockerfile skeleton
FROM scratch WORKDIR /site COPY cms /cms VOLUME /site EXPOSE 8080 USER 65534 CMD ["/cms"]
/site is the whole state: the root config, the sites with their databases, media, templates and TLS material, the node log under log/. Use a named volume so it survives container re-creation; the unprivileged user (65534) must be able to write it. To ship a prepared site instead of the bootstrap, set it up on the host (./cms def-help + your edits) and COPY --chown=65534:65534 site/ /site/.
Two deployment shapes
Port published directly (-p 8080:8080): docker bridge NAT masks every external client as the gateway IP. Keep trusted_proxies empty (the bootstrap default) — with the docker subnet trusted, anyone could spoof X-Real-IP and walk around rate limits.
Behind a proxy neighbour (nginx/caddy container on the same user-defined network, app port NOT published): add the proxy’s address or the network’s subnet to trusted_proxies in the root settings.json and make the proxy overwrite X-Real-IP (see the server article) — the app then sees real client addresses in logs and rate limits.
Notes
- Ports 80/443: map them (
-p 80:8080) or grant the binaryCAP_NET_BIND_SERVICE; a non-root user cannot bind them directly. --read-onlyroot filesystem is fine: everything the app writes — databases, media, thelog/folder, the reload socket./cms.sock— lives under the volumed working directory.- ACME/Let’s Encrypt: certificates live in the site’s
tls/inside the volume, so issued certs and renewals survive container restarts.