Developers

Docker on a VPS: The Basics of Running Containers in Production

A practical intro to Docker and Compose on a Linux VPS: images, containers, volumes, restart policies, safe port binding and the mistakes that cause trouble.

By Zebra Hosting Cloud

Docker packages an application together with its dependencies into an image and runs it as an isolated container. For developers this removes the "works on my machine" problem: the same image runs on your laptop and on the server. On a VPS, Docker is a convenient way to run several apps with different runtime versions side by side. It also adds new things to understand, which this article covers.

Containers in one minute

  • An image is a read-only template built from a Dockerfile or downloaded from a registry such as Docker Hub.
  • A container is a running instance of an image. Containers are disposable: delete one and start another from the same image.
  • A volume stores data outside the container, so it survives container removal.
  • Docker Compose describes several containers (web, database, cache) in one YAML file and starts them together.

Containers share the host's kernel, so they are lighter than virtual machines, but they are not as strongly isolated as a separate VM.

Installing Docker

Follow Docker's official installation instructions for your distribution at docs.docker.com, which add Docker's own package repository. The distribution's own docker.io package also exists but is often behind. The official packages include the Compose plugin, used as docker compose (with a space). Verify with:

sudo docker run --rm hello-world
docker compose version

Be careful with the docker group. Adding your user to it lets you run Docker without sudo, but anyone in that group effectively has root on the machine, since they can mount the host's filesystem into a container. Add only administrators you would trust with root.

A first Compose file

Create a folder for the project and a file named compose.yaml:

services:
  web:
    image: nginx:stable
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./site:/usr/share/nginx/html:ro
    restart: unless-stopped

Then run:

mkdir site && echo '<h1>Hello from a container</h1>' > site/index.html
docker compose up -d
docker compose ps
docker compose logs -f web

The -d flag runs in the background. docker compose down stops and removes the containers, and named volumes remain unless you add -v, so that flag deserves caution.

Building your own image

For your own application you describe the image in a Dockerfile. A minimal one for a Node.js app looks like this:

FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

Copying the package files first means Docker can reuse the cached dependency layer when only your source code changed, which speeds up rebuilds. Build and run it with docker build -t myapp . and a matching Compose service. A .dockerignore file should exclude node_modules, .git and any .env files so they never end up in the image.

Ports, data and secrets

Publishing ports safely

The line "127.0.0.1:8080:80" binds the container's port 80 to port 8080 on the loopback interface only. If you write just "8080:80", Docker listens on all interfaces and publishes the port to the internet. Docker also manipulates firewall rules itself, so such a published port can bypass rules you set with ufw. The safe pattern is the one in the example: bind to 127.0.0.1 and put Nginx on the host in front as a reverse proxy that terminates HTTPS.

Persistence, configuration and secrets

Anything written inside a container's own filesystem disappears when the container is removed. Put databases, uploads and other state in a volume or a bind-mounted folder, and include those locations in your backup plan. Pass settings through environment variables or an env_file, and keep that file out of Git. Plain environment variables are visible to anyone who can inspect the container, so for high-value secrets look at Docker's secrets mechanism or your orchestration tool.

Good habits and when Docker fits

  • Pin image tags. Use postgres:16 rather than latest, so an unplanned major upgrade does not appear when you recreate a container.
  • Use restart policies. restart: unless-stopped brings containers back after a reboot or crash.
  • Do not run as root inside. Use a USER instruction in your Dockerfile where the image allows it.
  • Keep images small. Multi-stage builds leave compilers and caches out of the final image.
  • Update deliberately. Rebuild or pull fresh images regularly so you receive security fixes; running containers do not update themselves.
  • Watch disk use. docker system df shows how much space images, containers and volumes use, and docker image prune removes unused images. Read what a prune command will delete before confirming.
  • Limit logs. Container logs can grow without bound by default, so configure log rotation in the daemon or the Compose file.

When Docker is, and is not, the right tool

Docker shines when you run several applications with conflicting requirements, need identical environments across machines, or want repeatable deployments. It is more than you need for a single WordPress site, where a control panel is simpler. On a very small VPS, remember that each container uses memory and that Docker itself has overhead; check capacity before stacking many services.

Updating containers

With Compose, the update routine is short: edit the image tag, then run docker compose pull followed by docker compose up -d. Compose recreates only the services whose image or settings changed. Take a backup of volumes before major version changes, particularly for databases, because a new image may migrate data in a way you cannot undo. See our security page for broader server protection advice.

Containers on Zebra Hosting

Docker needs a server where you control the operating system, which is what a Zebra Hosting VPS offers. For simpler websites, shared hosting with Plesk avoids server management altogether.