Docker
Ready-made images on Docker Hub, multi-arch (amd64, arm64, experimental riscv64) — docker pull picks your platform. latest and <version> are built FROM scratch: nothing in the image but the static binary. The -alpine tags add a shell for docker exec debugging.
Quick start
# 1. pull the image docker pull sqliteonlinecom/cmsnap:latest # 2. create the example site in a volume — the admin password is printed # right here, in your terminal docker run --rm -v mysite:/site sqliteonlinecom/cmsnap:latest /cmsnap def-help # admin login: admin # admin password: ................ # 3. start the server docker run -d --name mysite -v mysite:/site -p 80:8080 sqliteonlinecom/cmsnap:latest
Open http://<host>/ — the site is up; http://<host>/cms — sign in with the password from step 2.
Step 2 is optional. On an empty volume the container bootstraps the example site itself and prints the first-start block to stdout — admin password, the server-scope MCP token, and a one-time /cms enrollment link if the admin zone is gate-hidden. It is shown ONCE: read it with docker logs mysite.
The volume is the whole state
/site holds everything: the root settings.json, the sites with their databases, media, templates and TLS material, the node log under log/. Back it up as a folder, move it to another host, and the node comes back exactly as it was. Use a named volume so it survives container re-creation.
Run flags worth knowing
| flag | why |
|---|---|
-v mysite:/site | the state (see above). A bind-mounted host folder works too — then add --user "$(id -u):$(id -g)", because the image's unprivileged user (65534) cannot write a folder you own |
-p 80:8080 | the app binds 8080 inside — a non-root user cannot take 80. Map it, or grant the binary CAP_NET_BIND_SERVICE |
--network host | for busy sites: a published port goes through docker's NAT, which costs about HALF the throughput at high request rates. With host networking the engine binds the host stack directly and -p is not needed |
--restart unless-stopped | survives crashes and host reboots |
--cpus / --memory | cap the container. Caveat: the cache budgets itself from the HOST's free RAM (cgroup limits are invisible to it), so keep cache.mem_percent low enough that its budget stays under the cap |
--read-only | works as is: everything written — databases, media, log/, the reload socket — lives under the volume |
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 — the app then sees real client addresses in logs and rate limits. The upstream block with keepalive is what makes the difference: without it nginx opens a new TCP connection to the app for every request, capping throughput several times below what the engine serves.
upstream cms_up {
server cmsnap:8080; # the container name on the shared network
keepalive 64;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://cms_up;
proxy_http_version 1.1;
proxy_pass_header Server;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Use $remote_addr, never $http_x_real_ip — the latter forwards the client’s own header, a spoofing hole. The same block for a proxy on the host is in the server article.
Notes
- ACME/Let’s Encrypt: certificates live in the site’s
tls/inside the volume, so issued certs and renewals survive container restarts. - Baking a prepared site into your own image: prepare it on the host (
./cmsnap def-helpplus your edits), thenCOPY --chown=65534:65534 site/ /site/over the published image — the Dockerfile needs nothing else.