Running Docker on a Cloud Server: Setup to Production
Docker on a single cloud server is straightforward until the disk fills or a published port slips past the firewall. This guide installs the engine from the official repository, runs containers as real services, moves the stack into Compose, and fixes the gaps that break unattended hosts.

From a Laptop Image to a Server That Stays Up
Docker solves a narrow problem very well: it packages an application together with its dependencies so the thing you tested locally is the thing that runs remotely. On a single cloud server that removes most of the "works on my machine" arguments, and it makes rebuilding the box after an incident a matter of minutes rather than an afternoon.
What Docker does not do is run your service for you. A container that exits at three in the morning stays exited unless you told the daemon otherwise. Logs grow until the disk fills. Published ports can quietly punch through a firewall you thought was closed.
This walkthrough moves in that order: install the engine properly, run something, describe the stack in a Compose file, then close the gaps that separate a demo from a service you can leave alone for a month.
Installing the Engine Rather Than the Distribution Package
Most distributions ship a docker.io package that lags behind and omits the Compose v2 plugin. Use the official repository instead. Docker publishes per-distribution instructions in the engine installation documentation; on Ubuntu and Debian the repository route installs docker-ce, containerd and the docker-compose-plugin together.
Confirm the daemon is alive and the Compose plugin registered:
docker run --rm hello-world and docker compose version
Note the space in docker compose. The hyphenated docker-compose binary is the older Python implementation and you should not install it on a new machine.
Next, decide who may talk to the daemon. Adding a user to the docker group with sudo usermod -aG docker deploy is convenient, but that group grants effectively unrestricted control of the host, because a container can mount the root filesystem. On a server where several people have shell access, leave the group empty and require sudo docker so the commands land in the audit log, or run the engine in rootless mode.
Keep the daemon patched along with everything else. Unattended upgrades will pull new engine builds from the same repository you just added.
Running Something Real
A container started with a bare docker run is fine for a five minute experiment and wrong for anything else. Four flags turn it into a service.
docker run -d --name api --restart unless-stopped -p 127.0.0.1:3000:3000 --env-file /srv/api/.env ghcr.io/example/api:1.4.2
--restart unless-stopped brings the container back after a crash and after a host reboot, while still respecting a deliberate docker stop. Binding the published port to 127.0.0.1 means only a reverse proxy on the same host can reach it. --env-file keeps secrets off the command line, where they would otherwise sit in shell history and in docker inspect output for anyone on the box.
Pin the image to an exact tag, never latest. A digest is stronger still, because ghcr.io/example/api@sha256:... resolves to one specific build and a redeploy cannot silently pick up a different image.
Anything the application must keep across restarts belongs in a named volume or a bind mount. The writable layer of a container is disposable by design, and docker rm takes it with it.
Describing the Stack in a File
Once a second container appears, the flags stop fitting in your head. A compose.yaml file in the project directory records the same information in a form you can commit.
A typical single server stack is an application container, a database container with a named volume, and a proxy container holding the certificates. Compose puts them on a shared user defined network where they resolve each other by service name, so the application connects to db:5432 and nothing needs a published port except the proxy.
Day to day you need very few commands. docker compose up -d reconciles the running state with the file. docker compose pull followed by docker compose up -d is a deployment. docker compose logs -f api tails one service, and docker compose ps shows what the engine believes is running.
Keep image tags in an .env file beside the Compose file so a release is a one line change plus a pull. The Compose documentation covers the file reference, including depends_on with health conditions, which is what you want when the application container starts faster than the database accepts connections.
The Four Things That Break Unattended Servers
Logs that eat the disk
The default json-file driver writes container output to disk with no ceiling. Set a global limit in /etc/docker/daemon.json using log-driver and a log-opts block containing max-size and max-file, then restart the daemon. Existing containers keep their old settings until they are recreated.
Images that never leave
Every deployment leaves the previous image behind. docker system df shows the accumulation and docker image prune -af --filter "until=168h" clears anything older than a week. Put it in a weekly systemd timer rather than trusting yourself to remember.
A firewall that is not what it looks like
Publishing a port makes the engine write its own rules into the DOCKER chain, and those are evaluated before the rules that tools such as ufw manage. A container published on all interfaces is reachable from the internet even when ufw status suggests the port is closed. Bind to the loopback address as shown above, or use a provider firewall that filters before the packet reaches the host. The guide to cloud firewalls and security groups explains why the provider layer is the more reliable place to enforce this.
Containers with no ceiling
One runaway process can starve every other service on the machine. --memory and --cpus, or their Compose equivalents under resource limits, give the kernel permission to stop the offender instead of picking a victim at random.
Getting Images Onto the Server
Building on the production host works until the build itself consumes the memory the application needs. Build in continuous integration, push to a registry, and let the server only pull.
The registry can be Docker Hub, GitHub Container Registry, or a provider service such as Amazon ECR, Azure Container Registry, Google Artifact Registry or DigitalOcean Container Registry. Regional proximity matters more than brand: pulling a large image across an ocean on every deploy is slow and counts against transfer allowances. There is more on the options in the container registry overview.
Authenticate the server once with docker login using a token scoped to read only pulls. Then a deploy is three lines in a script: pull the new tag, run docker compose up -d, and probe the local health endpoint before you consider it done. Wiring that script into a pipeline is covered in the CI/CD article.
Docker runs the same way on nearly any Linux VM, which makes the choice of host mostly about price, location and network quality. A Hetzner account suits container hosts that need generous memory, while a DigitalOcean account pairs the Droplet with a managed registry in the same control panel. The DigitalOcean and Hetzner comparison sets out the trade between the two.
Questions That Come Up After the First Deploy
Should the database run in a container too?
For a staging or hobby stack, yes, with a named volume and a tested restore. For production data on a single host, many teams prefer a managed database instead, because the failure you cannot fix quickly is the one where the volume is gone and the last backup was never verified.
Do I need Kubernetes if I am already using Compose?
Not because of container count. Kubernetes earns its complexity when you need scheduling across several nodes, rolling updates with automated rollback, and a team that shares operational conventions. One server with Compose is a legitimate destination, not a stepping stone you must leave.
Why does my container see a different time zone or locale?
Because the image carries its own filesystem, including /etc/localtime. Set TZ in the environment, or mount the host file read only. The same logic explains missing CA certificates in slim base images: install the ca-certificates package in the image rather than expecting the host copy to appear.
How do I get a shell inside a running container?
Use docker compose exec api sh, or docker exec -it api sh for a plain container. Treat anything you change there as lost, because the next deploy replaces the container. If a fix needs to survive, it belongs in the image or in a mounted volume.
Questions about a cloud account?
Ask about products, variants, availability or purchasing on either channel.


