Guides+50 XP

Docker Compose guide for beginners: from zero to all your services in one file

What Docker Compose is, how to read a docker-compose.yml, everyday commands, volumes, ports and variables, with real examples from my home server.

Iván· Published on · 3 minLeer en español →

Every service on my Raspberry Pi lives in one file: docker-compose.yml. With it I start, stop, update or rebuild everything in seconds. If you’re getting started with Docker, Compose is the first thing you should learn after docker run.

What problem it solves

Without Compose, each container starts with a very long command:

docker run -d --name gitea --restart unless-stopped \
  -p 3000:3000 -p 2222:22 -v ./gitea:/data \
  -e USER_UID=1000 -e USER_GID=1000 docker.gitea.com/gitea:1.25

With five services, you end up forgetting which options you used. Compose keeps all that in a file that’s easy to read and can be versioned with Git.

Anatomy of a docker-compose.yml

services:
  gitea:                                  # service name
    image: docker.gitea.com/gitea:1.25    # image and version
    container_name: gitea                 # fixed container name
    restart: unless-stopped               # start automatically after reboot
    environment:                          # configuration variables
      USER_UID: "1000"
      USER_GID: "1000"
    ports:                                # host:container
      - "3000:3000"
      - "2222:22"
    volumes:                              # where data is stored
      - ./gitea:/data

  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    restart: unless-stopped
    ports: ["3001:3001"]
    volumes: [./uptime-kuma:/app/data]

It’s YAML: indentation (spaces, never tabs) matters. Let’s take it piece by piece.

image

The image and its tag (version). My advice: pin the version (:1.25, :2) instead of :latest. With latest, one day you pull and get a major version without warning.

restart

  • unless-stopped: starts automatically when the machine boots, unless you stopped it. It’s what I use almost always.
  • always: always starts, even if you stopped it.
  • no: never automatically (useful for one-off tasks).

ports

Format host:container. "8080:80" means: “whatever arrives on port 8080 of my machine, send it to port 80 inside the container”.

And a security trick: put an IP in front and it only listens on that interface:

    ports: ["127.0.0.1:8222:80"]   # only reachable from the machine itself

That’s how my password manager is set up: nobody on the network can reach it directly.

volumes

The most important part. A container is disposable: delete it and everything inside is lost. Volumes keep the data outside:

  • ./gitea:/data → the gitea folder next to the compose file is mounted at /data inside. This kind is the easiest to back up: backing up means copying the folder.
  • data:/data (no ./) → a named volume managed by Docker. Cleaner, but you need to know where it lives to back it up.
  • /path:/path:ro → :ro mounts it read-only.

environment

Variables the program reads to configure itself. Each image documents its own.

For passwords and secrets, don’t write them in the compose file (they’ll end up in Git). Use an .env file next to it that you don’t commit:

    environment:
      DB_PASSWORD: ${DB_PASSWORD}
# .env (not committed to Git)
DB_PASSWORD=something-long-and-secret

Everyday commands

Always from the folder containing docker-compose.yml:

Command What it does
docker compose up -d Create and start everything in the background
docker compose up -d gitea Just that service
docker compose ps What’s running
docker compose logs -f gitea Follow the logs live (Ctrl+C to exit)
docker compose stop Stop without deleting
docker compose down Stop and delete the containers (volume data stays)
docker compose pull Download new image versions
docker compose exec gitea sh Open a shell inside the container

Updating a service

docker compose pull uptime-kuma
docker compose up -d uptime-kuma

up -d notices the image changed and recreates the container with the new one. The data, in its volume, is still there. Afterwards, to free space from old images:

docker image prune

Common errors

  • “port is already allocated”: another program is using that port. Find it with sudo ss -ltnp | grep :PORT and change the left-hand port.
  • Permissions on the data folder: many containers run as a specific user (hence the PUID/PGID or USER_UID variables). If it can’t write, check who owns the folder.
  • Tabs in the YAML: syntax error. Spaces only.
  • Forgetting the volume: everything works… until you recreate the container and the data vanishes. Always check where the image stores its data.

How I organise it

~/servicios/
├── docker-compose.yml
├── .env
├── gitea/
├── uptime-kuma/
├── homepage/
└── vaultwarden/

One file, one folder per service. Backing up means copying ~/servicios, and rebuilding the server on another machine means copying the folder and running docker compose up -d.

Mini quiz

Did it stick?

Three quick questions. Each right answer is worth 10 XP.

  1. In ports: ["8080:80"], which number is the host's?
  2. What happens to data if you delete a container without a volume?
  3. What does docker compose up -d do?
  • #docker
  • #docker-compose
  • #containers
  • #yaml
  • #homelab
Esc