10 KiB
Docker
RenewCMS runs as a container. The image is published to the registry at git.floriansylvain.fr/fsylvain/renewcms, so site operators pull it and never build from source; it can also be built from this repository. The image holds only the single binary (SPA embedded) on Alpine, runs as a non-root user (uid 10001) and keeps all state on one volume mounted at /data. It reuses the configuration contract described in configuration.md; there are no Docker-specific variables.
Requirements: Docker with Compose v2. task is optional for operators and needed to build from source or to publish. Building needs no local Go or Bun. The image build bundles the SPA with vite build only; the vue-tsc type-check is not run inside the image (it does not work under the Bun-only build image), so keep running bun run type-check locally.
Quick start
-
Copy the example env file and edit it:
cp docker.env.example docker.envSet
PUBLIC_URL, theSMTP_*values and a generated secret (openssl rand -hex 32) forJWT_SECRET.docker.envis git-ignored and excluded from the build context, so it never ends up in an image. Never commit it. -
Pull and start the published image:
docker compose pull docker compose up -ddocker-compose.ymlusesimage: ${RENEWCMS_IMAGE:-git.floriansylvain.fr/fsylvain/renewcms:beta}. The default tag isbetabecauselatestdoes not exist until the first stable release (the default changes tolatestthen). SetRENEWCMS_IMAGEin your shell or in a.envfile next to the compose file to pin another tag, for exampleRENEWCMS_IMAGE=git.floriansylvain.fr/fsylvain/renewcms:v2.9.0-beta.To build from the local sources instead, see Building from source.
-
Check it:
curl http://localhost:8080/healthzreturnsok, andhttp://localhost:8080/serves the SPA.
Other tasks: task docker:pull, task docker:upgrade (pull, then docker compose up -d), task docker:up (local build, see below), task docker:build (image only, tagged with the git describe version and latest), task docker:run (one-off foreground run on the volume renewcms-data, without Compose), task docker:logs and task docker:down.
If a required variable is missing the container exits non-zero and the log names it, for example Environment variable JWT_SECRET is not set.
Local trial over plain HTTP
ENVIRONMENT defaults to production, which makes the auth cookie Secure: browsers do not store it over http://, so login appears to fail. For a local trial on http://localhost:8080, set ENVIRONMENT=development and PUBLIC_URL=http://localhost:8080 in docker.env. Use production only behind HTTPS.
Building from source
docker-compose.build.yml adds the build: section and tags the result renewcms:${VERSION:-dev}. Use it as an override:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
task docker:up does the same and passes the git describe version as VERSION. To check what Compose resolves, run docker compose config: without the override it shows the registry image and no build section, with it the build section is present.
Image tags
| Release | Tags pushed |
|---|---|
vX.Y.Z (stable) |
vX.Y.Z, X.Y.Z, X.Y, latest |
vX.Y.Z-<pre> (for example v2.9.0-beta, v2.9.0-rc.1) |
vX.Y.Z-<pre>, X.Y.Z-<pre>, and the moving tag <pre-name> (beta, rc) |
A pre-release never moves latest. Images are published for linux/amd64 and linux/arm64. renewcms --version in the image prints the exact release tag. Images are not signed; the Ed25519 key signs the release binaries only.
Environment
Compose reads docker.env through env_file and sets DB_FILE=/data/renewcms.db and UPLOAD_DIR=/data/uploads itself. Everything in configuration.md can go in docker.env. To publish another host port, change the left side of ports in docker-compose.yml; keep PORT at 8080 unless you also change the healthcheck.
The container receives SIGTERM on stop, and RenewCMS shuts down gracefully within 10 seconds. Compose uses stop_grace_period: 15s. The healthcheck calls /healthz inside the container.
Data volume
State lives on the named volume renewcms-data (Compose prefixes it with the project name, usually renewcms_renewcms-data; check with docker volume ls). It holds the SQLite database, uploads, and update records.
docker compose downremoves the container and keeps the volume. The data survives anddocker compose up -dbrings it back.docker compose down -vdeletes the volume and all data. The next start is an empty instance.
Backup
Replace VOLUME with the real volume name. The image has no sqlite3, so stop the service for a consistent copy:
docker compose stop
docker run --rm -v VOLUME:/data -v "$PWD":/backup alpine tar czf /backup/renewcms-backup.tar.gz -C /data .
docker compose start
Restore into a fresh volume
docker compose down -v
docker volume create VOLUME
docker run --rm -v VOLUME:/data -v "$PWD":/backup alpine sh -c "tar xzf /backup/renewcms-backup.tar.gz -C /data && chown -R 10001:10001 /data"
docker compose up -d
Reverse proxy and TLS
With ENVIRONMENT=production the site must be served over HTTPS, so put a reverse proxy in front and set PUBLIC_URL to the public https:// address. Bind the published port to the host only when the proxy runs on the same host, by changing ports to "127.0.0.1:8080:8080".
Caddy on the host:
cms.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up X-Real-IP {remote_host}
}
}
nginx on the host:
server {
listen 443 ssl;
server_name cms.example.com;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Set TRUSTED_PROXIES to the proxy address as the container sees it, otherwise the login rate limiter ignores X-Real-IP and counts every client as the proxy. With a proxy on the host and the default bridge network, that is the bridge gateway (check with docker network inspect, often 172.17.0.1 or the Compose network gateway). With a proxy container on the same Compose network, it is that container's IP. The value is a semicolon-separated list of exact IPs, no CIDR. See deployment.md.
SMTP
RenewCMS sends registration and password-reset mail through the SMTP_* variables. Use your provider's host and port in docker.env.
To use a mail server running on the Docker host, set SMTP_HOST=host.docker.internal. Docker Desktop resolves that name. On Linux add the mapping to the service in docker-compose.yml:
extra_hosts:
- "host.docker.internal:host-gateway"
The host MTA must listen on an address reachable from the container, not only on 127.0.0.1.
If SMTP is unreachable, the server still starts. Only the actions that send mail (registration, password reset, invitations) fail, and the error appears in docker compose logs renewcms.
Upgrading
-
Back up the volume (see above).
-
Pull the new image and recreate the container on the same volume:
docker compose pull docker compose up -dtask docker:upgraderuns both. The schema is migrated at startup, and there is no down-migration. -
Check
docker compose logs renewcmsfor the new version line, ordocker run --rm git.floriansylvain.fr/fsylvain/renewcms:beta --version. The owner's Settings page also shows the running version.
To roll back, set RENEWCMS_IMAGE to the previous tag, run docker compose up -d, and restore the backup if the schema changed.
If you built from source, upgrade with git pull and task docker:up instead.
Maintainer: publishing
Publishing is a manual, maintainer-run task, like task release. There is no CI pipeline.
-
Tag the release and keep the work tree clean:
git tag v2.9.0-beta -
Log in once to the registry. The task never reads or stores credentials:
docker login git.floriansylvain.fr -
Make sure a buildx builder can build both platforms. Check with
docker buildx ls. With the containerd image store (the Docker Desktop default) the default builder already works. Otherwise create one, and on a Linux host install the QEMU emulators:docker buildx create --use docker run --privileged --rm tonistiigi/binfmt --install arm64 -
Publish:
task docker:publishREGISTRY_IMAGEdefaults togit.floriansylvain.fr/fsylvain/renewcmsandVERSIONtogit describe --tags. The task refuses to run, before building anything, whenREGISTRY_IMAGEis empty, whenVERSIONis not an exact tagvX.Y.ZorvX.Y.Z-<pre>on HEAD, when the work tree is dirty, or when the builder cannot buildlinux/amd64andlinux/arm64. It prints the tags it pushes.
Dry run
task docker:publish -- --dry-run
task docker:publish VERSION=v2.9.0-rc.1 -- --dry-run
The dry run builds for the host platform only, loads the image into the local Docker, pushes nothing and needs no login. It prints the tags a real run would push. A VERSION override is accepted only with --dry-run, and a dirty tree is allowed there. Remove the loaded images afterwards with docker rmi.
After pushing
docker buildx imagetools inspect git.floriansylvain.fr/fsylvain/renewcms:<tag>listslinux/amd64andlinux/arm64.- The moving tag (
beta) points at the new digest, andlatestdid not move for a pre-release. - On a clean machine,
docker compose pull && docker compose up -dstarts the new image and Settings shows the new version.
Limitations
Browser updates are unsupported in Docker (they need Linux with systemd, and a binary replaced inside a container is lost when it is recreated). The image and docker-compose.yml set RENEWCMS_CONTAINER=true, so the owner's Settings page shows when a newer version exists and tells them to pull a new image: docker compose pull && docker compose up -d. There is no Update or Restart button. Upgrade by pulling the image as above, or by rebuilding it if you build from source.