Running Docker on a Cloud Server, Properly
Containers on a single server are an excellent, underrated production setup. There are about eight details that separate a good one from a fragile one.
Not everything needs an orchestrator. Docker Compose on one well-configured server runs a substantial application, deploys in seconds and can be understood in full by one person. That is worth more than it sounds.
Set the host up first
- Install Docker from the official repository, not the distribution package, which is often well behind.
- Add your user to the
dockergroup, and understand that this is equivalent to root access on the host. - Configure log rotation in
daemon.json, or container logs will fill the disk. This is the most common single-server failure there is. - Set up a firewall — and note that Docker manipulates iptables directly, so published ports can bypass
ufwrules unless you account for it. - Put containers on a private Docker network and publish only what must be public.
The ufw interaction surprises people regularly: a container published with -p 5432:5432 is reachable from the internet even when ufw appears to deny it. Bind to 127.0.0.1 explicitly when a port should be local only.
Compose as the deployment unit
A single compose.yaml describing the whole stack — application, database, cache, reverse proxy — is readable, version-controllable and reproducible. For one server it is the right level of abstraction.
- Pin image tags to versions, never
latest, or a redeploy silently changes what runs. - Set
restart: unless-stoppedso containers survive a reboot. - Declare health checks, so dependent services wait for readiness rather than crash-looping.
- Use named volumes for data, not bind mounts into arbitrary paths.
- Keep environment values in a
.envfile that is not in the repository. - Set memory and CPU limits, so one container cannot starve the host.
Images: small, layered, reproducible
- Use multi-stage builds so build tools do not ship in the final image.
- Start from a slim or Alpine base where compatible.
- Order instructions so rarely-changing layers come first and caching works.
- Copy dependency manifests and install before copying source, so a code change does not reinstall everything.
- Never copy secrets into an image — they persist in the layer history even if deleted later.
- Run as a non-root user inside the container.
- Add a
.dockerignore, or you will copy.gitandnode_modulesinto every build.
Data and backups
Containers are disposable; their data must not be. Named volumes survive container replacement, and they live on the host, so they need the same backup treatment as any other data.
- Back up volumes on a schedule, not only the host disk.
- For databases, use the database’s own dump command rather than copying files from a running container.
- Store backups off the host.
- Test a restore into a fresh container occasionally.
Logging and disk
By default Docker’s json-file driver keeps logs indefinitely. On a busy service this fills the disk, which stops everything at once. Set it explicitly:
In /etc/docker/daemon.json, configure the json-file driver with a max-size such as 10 MB and max-file of 3. Apply it globally so every container inherits it, and restart Docker for it to take effect.
Add docker system prune on a schedule to remove unused images and build cache, which otherwise accumulate steadily.
Deploying updates
- Build the image in CI and push it to a registry, tagged with the commit.
- On the server, pull the new tag and run
docker compose up -d. - Compose replaces only changed containers, so unaffected services keep running.
- For a zero-downtime web tier, run two replicas behind the reverse proxy and replace them one at a time.
- Keep the previous tag available so rollback is a single command.
This is a simple, reliable deployment mechanism that a small team can operate confidently, which matters more than sophistication.
Common questions
Is Docker Compose suitable for production?
On a single server, yes, and many production systems run this way. It lacks multi-node scheduling and self-healing across hosts, which is exactly when you would move to an orchestrator.
Should the database run in a container?
It works, with a named volume and proper backups. A managed database or a database on the host is often preferable for production data, because the operational stakes are higher.
How do I handle HTTPS?
A reverse proxy container — Caddy, Nginx with Certbot, or Traefik — handling certificates and routing to the application containers over the internal network.
Why is my disk full?
Almost always container logs without rotation, or accumulated images and build cache. Configure log limits and schedule a prune.