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 downEnv 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.
Related products
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.
