Deploy with Docker
In this guide you make the Docker stack from the getting-started guide fit for production: a real database password, a fixed secret, your public address and a proxy-only port. You add an override file.
Before you start
- Docker with Compose 2.24.4 or later (
docker compose version). - The
Dockerfile,.envanddocker-compose.ymlfrom Install with Docker, in one directory.VERSIONin.envselects the release that is downloaded and is whathatamoto versionprints. It must have the formvX.Y.Z, with the leadingv; find the current one as described in Install the binary. - A domain name. You add HTTPS next, in Put a reverse proxy in front.
- SMTP credentials, if the site sends email. Enter them at Settings > Email, or as
SMTP_*variables in the override file below. See Set up email.
Steps
Know what you are running
- Read what the image sets. It is
debian:bookworm-slimwith CA certificates and thehatamotoprogram added, and the source tree's ownDockerfilebuilds the same image from source. The command issupervise: the supervisor is the process that starts the server, watches it and switches to a new release after an update. It runs as PID 1 with no shell or entrypoint script, so if it exits, Docker restarts the container.Item Value Program /usr/local/bin/hatamotoEntry point and command ENTRYPOINT ["hatamoto"],CMD ["supervise"]User system user app, uid10001, no shell, no home, so the process never runs as rootWorking directory /appData directory HATAMOTO_DATA=/data, a declaredVOLUMESecrets directory HATAMOTO_SECRETS_DIR=/data/secretsUploads /app/uploads(the defaultMEDIA_UPLOAD_DIRis./uploads, relative to/app)Port EXPOSE 8090Health check hatamoto healthcheckevery 30 seconds, 5 second timeout, 30 second start period, 3 retries - Know the four named volumes, and back them all up (see Back up and restore). The
secretsvolume sits inside/dataso the secret survives ifdatais recreated. Do not remove it.Volume Mounted at Holds pgdata/var/lib/postgresql/data(indb)The Postgres data files. data/dataReleases, update state and backups, compiled plugin cache. secrets/data/secretssecrets.envwith the generated server secret.uploads/app/uploadsUploaded media files.
Make it production
- Put real values in
.envnext to the compose file, besideVERSION, and keep the file out of version control.HATAMOTO_SECRETis the server secret, a long random value the server uses to sign links and protect stored credentials; it must stay the same across restarts. Avoid@,:,/,?and#in the database password, because it is pasted into a URL:VERSION=vX.Y.Z DB_PASSWORD=<database-password> HATAMOTO_SECRET=<at-least-32-random-characters> - Create
production.ymlbesidedocker-compose.yml. Compose joinsportslists from several files:!overridereplaces the base list, so Hatamoto listens on loopback only, for the proxy, and!reset []empties it, so the database has no published port.ENVIRONMENT: productionturns on JSON logs and the HSTS header, so only use it behind HTTPS.services: db: environment: POSTGRES_PASSWORD: ${DB_PASSWORD} ports: !reset [] hatamoto: environment: ENVIRONMENT: production DATABASE_URL: postgres://hatamoto:${DB_PASSWORD}@db:5432/hatamoto?sslmode=disable PUBLIC_BASE_URL: https://example.com HATAMOTO_SECRET: ${HATAMOTO_SECRET} SMTP_HOST: smtp.example.com SMTP_FROM: [email protected] HATAMOTO_TRUSTED_PROXIES: 172.18.0.1 ports: !override - "127.0.0.1:8090:8090" - If you already ran the demo, change the database password in place, because
POSTGRES_PASSWORDonly takes effect whenpgdatais created. Or start from an empty volume withdocker compose down -v, which deletes the demo data.docker compose exec db psql -U hatamoto -c "ALTER ROLE hatamoto PASSWORD '<database-password>'"HATAMOTO_TRUSTED_PROXIESmust be the address your proxy connects from. A host Caddy or nginx reaching the published port arrives from the bridge gateway, not loopback, so without it every visitor looks like one client andX-Forwarded-*is ignored (the log shows a one-time warning). Trust the gateway only while the port is published on127.0.0.1, asproduction.ymlabove does. If the port is open on every interface ("8090:8090", as in the getting-started file), remote clients also arrive from the gateway and can forgeX-Forwarded-For. Read the gateway withdocker network inspect <project>_default --format '{{(index .IPAM.Config 0).Gateway}}', and use the proxy container's address instead if the proxy is a Compose service. See Reverse proxy and TLS. - Check the merged configuration.
hatamotomust list onlyhost_ip: 127.0.0.1, anddbmust have noportsat all:docker compose -f docker-compose.yml -f production.yml config - Start it. Pass both files to every later command (
exec,logs,down); Compose reads.enveach time. Keepstop_grace_periodat 45 seconds or more: the supervisor stops the server within 40 seconds.docker compose -f docker-compose.yml -f production.yml up -d - Leave
CSP_ENFORCE: "false"from the base file for now, so pages send a report-only policy. Set it to"true"inproduction.ymlonce your pages show no violations.
Run commands and update
- Run operator commands inside the container. They run as
app, so a bind-mounted/datamust be owned by uid10001:docker compose -f docker-compose.yml -f production.yml exec hatamoto hatamoto migrate status - To update, let the supervisor install signed releases into the
datavolume, or set a newerVERSIONand runup -d --build.HATAMOTO_UPDATESdecides the policy; see How updates work. An image built from a source checkout takes--build-arg VERSION=vX.Y.Z --build-arg COMMIT=$(git rev-parse HEAD); without a propervX.Y.Zit counts as a development build and never updates itself.
Check it worked
On the server, ask the readiness endpoint, which also checks the database:
curl -s http://127.0.0.1:8090/ready
ok
docker compose ps shows the hatamoto service as healthy after the start period.
If the log says password authentication failed, the role still has the demo password: change it as in step 3 of Make it production, then start again. If the log says HATAMOTO_SECRET must be at least 32 characters, make the value in .env longer.