🛠️ ITBlogs.pro

Docker Compose Tutorial: Run a Multi-Container App

Oct 16, 2026 · Docker · 4 min read

Docker Compose describes a whole application, with its containers, networks and volumes, in one YAML file. Instead of five long docker run commands, you write the setup down once and start it with a single command.

Install check

Compose ships with Docker Engine as a plugin:

docker compose version

Use docker compose (with a space). The old docker-compose binary is deprecated.

A realistic example

Create a project folder with a compose.yaml and a .env file.

# compose.yaml
services:
  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./site:/usr/share/nginx/html:ro
    depends_on:
      db:
        condition: service_healthy

volumes:
  db_data:
# .env  (never commit this file)
DB_ROOT_PASSWORD=change-me-long-random
DB_PASSWORD=change-me-too

Generate real passwords with openssl rand -hex 16, and add .env to .gitignore.

What the file means

Key Purpose
services One entry per container. The name (db, web) becomes its hostname on the network
image / build Use a ready image, or build from a Dockerfile (build: .)
ports HOST:CONTAINER. Prefixing 127.0.0.1: keeps it private
volumes db_data: is a named volume (persistent). ./site:/... is a bind mount of a host folder
environment Variables for the container. ${VAR} is read from .env
depends_on + healthcheck Start web only after db is actually ready, not just started
restart Come back after a crash or reboot

The top-level version: key is obsolete, so leave it out.

Because services share a network, your app connects to the database at host db, port 3306. You don't need to publish the database port at all, and you generally shouldn't.

Everyday commands

docker compose up -d              # create and start everything in the background
docker compose ps                 # status
docker compose logs -f web        # follow one service's logs
docker compose exec web sh        # shell in a running service
docker compose restart web
docker compose stop               # stop, keep containers
docker compose down               # stop and remove containers + network (volumes are kept)
docker compose down -v            # ALSO delete volumes, which means your database is gone
docker compose config             # validate and print the final merged config

Updating to newer images

docker compose pull
docker compose up -d              # recreates only the containers whose image changed
docker image prune -f             # remove the old image layers

If you use build:, rebuild with docker compose up -d --build.

Back up the database

docker compose exec db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --all-databases' \
  | gzip > backup-$(date +%F).sql.gz

Restore:

gunzip -c backup-2026-10-16.sql.gz | docker compose exec -T db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD"'

Common mistakes

  • Data disappears after down. You used a container path with no volume, or you ran down -v. Use named volumes for anything that must persist.
  • "Connection refused" to the database at startup. The app started before the DB was ready. Use healthcheck + depends_on: condition: service_healthy.
  • Using localhost between containers. Inside a container, localhost is itself. Use the service name (db).
  • Changed .env but nothing changed. Re-run docker compose up -d so containers are recreated with the new values.
  • Port already in use. Another service owns the host port. See which process is using a port.
  • Secrets in the compose file. Keep them in .env or Docker secrets, not in Git.

Put it behind a reverse proxy

For several sites on one server, bind each app to 127.0.0.1:PORT as above and let Apache or Nginx handle HTTPS and domain names in front. Your containers stay unreachable from the internet except through the proxy.