On this page
Starting containers is only the beginning
A local Compose project may use source-code mounts, development ports, latest images, and default passwords. Those conveniences become uncertainty in production. Changing an environment variable to production is not enough. You need to know where images come from, where data lives, how containers communicate, how failure is detected, and how to return to a previous version.
Keep a base compose.yaml and use compose.production.yaml for production differences. This makes the boundary reviewable without maintaining two full files that slowly diverge.
Use images you can identify and retrieve again
Pin a version or immutable digest, such as myapp:2026-08-14, rather than relying on latest, whose contents change over time. If you build on the server, pin the base image and use a lockfile. Check that the build context does not include .env, databases, or backups. A controlled build followed by pulling the same image on the server can be easier to reproduce.
Record the image ID and release version. A rollback needs the last verified image, not a fresh build of old code; base images and dependency sources may have changed in the meantime.
Publish only the port the reverse proxy needs
When Caddy runs on the host, bind the application port to loopback:
services:
app:
image: registry.example.com/myapp:2026-08-14
ports:
- "127.0.0.1:3000:3000"
restart: unless-stoppedThe database does not need a published ports entry; containers can reach it by service name on an internal Compose network. EXPOSE in an image is not the same as publishing a host port. Check actual listeners with ss -lntp and docker compose ps.
Know exactly which data must survive container replacement
List every writable location: database, uploads, search indexes, queues, and generated application files. Give bind mounts explicit host paths and permissions. For named volumes, document where their data is and how to back it up. Removing a container should not remove durable data, but docker compose down -v removes volumes; a production runbook must not treat it as routine cleanup.
Mount read-only configuration separately from persistent data. If containers run as non-root users, set directory ownership ahead of time. Do not respond to a permission error by making a data directory world-writable.
A health check should test a useful capability
A running process may still be unable to serve requests. Provide a lightweight /health endpoint that confirms essential initialization without running an expensive full-table database query every few seconds. Set sensible interval, timeout, retries, and startup grace period:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20sCheck that the image actually contains wget; otherwise use a tool it does contain. Dependency ordering may reduce startup races, but the application still needs to handle unavailable dependencies and retries.
Inspect the rendered configuration before starting
docker compose -f compose.yaml -f compose.production.yaml config
docker compose -f compose.yaml -f compose.production.yaml pull
docker compose -f compose.yaml -f compose.production.yaml up -d
docker compose ps
docker compose logs --tail=100 appThe config output reveals final ports, mounts, and resolved variables. Inspect it without copying secrets into an issue or chat. After starting, test the local health endpoint, the Caddy domain, and an essential application route.
Plan updates and rollback before the first release
Back up data before deployment, pull the new image, wait for health, and then switch or continue serving traffic. If the new version fails, restore the previous image tag and run up again rather than trying to rebuild an old Git commit during an incident. Check database migrations for backward compatibility. A data change that cannot be rolled back may require an expand-then-switch-then-clean-up sequence.
You are ready when images are identifiable, public ports are minimal, persistent data is listed and backed up, health checks really work, the final configuration has been reviewed, and the previous image remains available. Next, protect the part most likely to leak through files or logs: production secrets.
Sources
Next steps
Continue with the next useful task; you do not need to read everything at once.
- Install Ollama →
- Deploy on One Server with Health Checks and a Rollback Plan →
- Deploy Open WebUI with Docker and Persistent Storage →
- Deploy Dify with Docker Compose: A Production-minded Guide →