OrbitHost
Guides

Docker Compose Tutorial for Ubuntu VPS

Learn Docker Compose on Ubuntu 24.04: compose.yaml structure, networks, volumes, env files, and a practical reverse-proxied multi-service example.

By Alex Rivera · Infrastructure Engineer10 min read
Author
Alex Rivera
Infrastructure Engineer

Alex designs and hardens EU VPS fleets in Frankfurt and Amsterdam. He spends most days on Docker, KVM networking, NVMe layouts, and keeping Ubuntu 24.04 boxes boringly reliable for production workloads.

Mental model

A Compose file declares services, networks, and volumes. `docker compose up -d` creates them as a project. One VPS can host many projects with isolated networks.

Install Engine first using our Ubuntu 24.04 Docker guide on an OrbitHost KVM VPS.

Minimal web + redis example

This stack runs a tiny web service and Redis on a private network. Only the web port is published.

# /opt/demo/compose.yaml
services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    depends_on:
      - cache
    networks:
      - internal
    restart: unless-stopped
  cache:
    image: redis:7-alpine
    networks:
      - internal
    restart: unless-stopped
networks:
  internal:

Day-one commands

Learn the short loop you will use forever: up, ps, logs, exec, down. Prefer `down` without `-v` unless you intend to delete volumes.

cd /opt/demo
mkdir -p html && echo '<h1>hello</h1>' > html/index.html
docker compose up -d
docker compose ps
docker compose logs -f web
docker compose exec cache redis-cli ping
docker compose down

Env files and secrets

Use `.env` next to compose.yaml for interpolating `${POSTGRES_PASSWORD}` style values. Do not commit real secrets. For sensitive production, consider Docker secrets or an external vault later.

# .env
POSTGRES_PASSWORD=change_me
POSTGRES_DB=app

# compose snippet:
# environment:
#   POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Volumes vs bind mounts

Named volumes are best for database data. Bind mounts are best for live-editing config/html on the host. Mixing them intentionally keeps disasters recoverable.

  • Named volume → Postgres/MySQL data
  • Bind mount → nginx config, static site
  • Backup volumes explicitly
  • Never delete `-v` casually in prod

Adding a reverse proxy

Publish only 80/443 on a Caddy or Traefik service; keep app services off host ports. That pattern scales into Coolify-managed deploys later.

services:
  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
    networks:
      - internal
  app:
    image: ghcr.io/example/app:1.2.3
    networks:
      - internal
    restart: unless-stopped
networks:
  internal:
volumes:
  caddy_data:

Production habits

Pin image tags, healthchecks, resource limits (`mem_limit` / deploy resources depending on compose spec), and log rotation. Monitor disk — images and volumes fill NVMe quietly.

When YAML fatigue hits, move to Coolify/Dokploy on the same OrbitHost Docker VPS class without abandoning Compose knowledge — panels still generate containers underneath.

FAQ

Is docker-compose dead?

The standalone binary is legacy. Use `docker compose` (plugin) shipped with Docker Engine installs from Docker’s apt repo.

Compose vs Swarm/Kubernetes?

Compose is perfect for one host. Swarm/K8s enter when you need multi-node scheduling. Most OrbitHost single-VPS apps never need K8s.

Where should compose files live?

`/opt/stackname` or a git repo cloned per environment. Keep `.env` out of git.

How do I update images?

Pin tags, `docker compose pull`, then `docker compose up -d`. Read changelogs for breaking env vars.

Ready to Launch?

Create your server today.