$linuxjunkies
>

How to Set Up and Use Docker Compose

Learn how to write Compose files, define networks and volumes, manage environment variables, and control multi-container Docker stacks with the modern Compose CLI plugin.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • A Linux system with internet access and sudo privileges
  • Basic familiarity with the Docker CLI and container concepts
  • At least 2 GB of free disk space for images and volumes

Docker Compose turns multi-container applications into a single declarative file. Instead of typing long docker run commands with ports, volumes, and environment variables threaded together, you describe every service in a compose.yaml file and manage the whole stack with one command. This guide covers installation, writing a real Compose file, networking, persistent volumes, environment variables, and day-to-day lifecycle operations.

Installing Docker Compose

Modern Docker ships Compose as a CLI plugin (docker compose, no hyphen). The legacy standalone binary (docker-compose) is deprecated. Use the plugin unless you have a specific reason not to.

Debian / Ubuntu

sudo apt update
sudo apt install -y docker.io docker-compose-plugin

Fedora / RHEL 9 / Rocky 9

sudo dnf install -y docker docker-compose-plugin
sudo systemctl enable --now docker

Arch Linux

sudo pacman -S docker docker-compose
sudo systemctl enable --now docker

Verify the plugin is available:

docker compose version

Output will look similar to: Docker Compose version v2.27.0 — exact version will vary.

Add your user to the docker group so you can run commands without sudo:

sudo usermod -aG docker $USER
newgrp docker

Compose File Structure

Compose reads compose.yaml (preferred) or the legacy docker-compose.yml from the current directory. The top-level keys are services, networks, and volumes.

Here is a realistic example: a Node.js application backed by PostgreSQL and a Redis cache.

mkdir myapp && cd myapp
nano compose.yaml
services:
  app:
    image: node:20-alpine
    working_dir: /usr/src/app
    volumes:
      - ./src:/usr/src/app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://appuser:secret@db:5432/appdb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: node server.js

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

networks:
  default:
    name: myapp-net

Key points in this file:

  • depends_on with condition — the app service waits for the database healthcheck to pass before starting, preventing connection errors on startup.
  • Named volumes (pgdata, redisdata) persist data across docker compose down and container rebuilds.
  • Service DNS — services reach each other by service name. The app connects to Postgres at host db, not localhost.

Managing Environment Variables

Hardcoding secrets in compose.yaml is fine for local dev but wrong for production or shared repos. Use an .env file instead.

cat > .env <<'EOF'
POSTGRES_USER=appuser
POSTGRES_PASSWORD=s3cur3p4ss
POSTGRES_DB=appdb
NODE_ENV=production
EOF

Compose automatically loads .env from the project directory. Reference variables with ${VAR} syntax in the Compose file:

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Add .env to .gitignore immediately:

echo ".env" >> .gitignore

You can also use env_file to load variables from a named file per service, which is useful when services need different secret sets:

  app:
    env_file:
      - ./config/app.env

Networks

Compose creates a default bridge network for each project. All services on that network resolve each other by service name. The explicit networks block in the example above just gives it a predictable name — useful when external tools or other Compose stacks need to join.

To connect services across two separate Compose stacks, declare one network as external:

networks:
  shared-net:
    external: true

Create it once manually:

docker network create shared-net

Services on isolated internal networks (no internet access) use internal: true:

networks:
  backend:
    internal: true

Volumes in Depth

Compose supports three volume types:

  • Named volumes — managed by Docker, survive compose down, best for databases. Declared under the top-level volumes: key.
  • Bind mounts — map a host path directly into the container. Ideal for source code during development (./src:/usr/src/app).
  • tmpfs — in-memory only, lost on container stop. Useful for sensitive temporary data.

List all named volumes:

docker volume ls

Inspect where a named volume actually lives on the host:

docker volume inspect myapp_pgdata

Warning: docker compose down -v deletes named volumes permanently. Use it only when you want a clean slate.

Bringing Services Up and Down

Start the stack

docker compose up -d

The -d flag runs containers in detached mode. Compose pulls missing images, creates networks and volumes, and starts services in dependency order.

View running services and logs

docker compose ps
docker compose logs -f
docker compose logs -f app

Restart a single service

docker compose restart app

Rebuild images after a Dockerfile change

docker compose up -d --build app

Scale a stateless service

docker compose up -d --scale app=3

Only works cleanly when the service does not bind a fixed host port.

Stop without removing containers

docker compose stop

Stop and remove containers and networks (keep volumes)

docker compose down

Full teardown including volumes

docker compose down -v

Running One-Off Commands

Execute a command inside a running service container:

docker compose exec db psql -U appuser -d appdb

Spin up a temporary container for a service (does not start dependent services):

docker compose run --rm app node --version

Verification

After docker compose up -d, confirm everything is healthy:

docker compose ps

All services should show running (or healthy for services with a healthcheck). Check the app is reachable:

curl -I http://localhost:3000

Inspect inter-service DNS resolution from inside a container:

docker compose exec app ping -c 2 db

Troubleshooting

  • Service exits immediately — run docker compose logs <service> to see the error. Often a missing environment variable or failed entrypoint script.
  • Port already in use — another process owns the host port. Find it with ss -tlnp | grep :3000 and stop it, or change the host port in compose.yaml.
  • depends_on not waiting long enough — use the condition: service_healthy form with a proper healthcheck block, not the default service_started.
  • Volume permission errors — the container process UID may not match the bind-mount directory owner. Fix with chown on the host or a user: directive in the service definition.
  • Changes to compose.yaml not picked up — always run docker compose up -d again after edits; Compose detects and recreates only changed services.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

What is the difference between `docker compose down` and `docker compose stop`?
`stop` halts running containers but leaves them and their networks in place so you can restart them quickly. `down` removes the containers and networks entirely; named volumes are kept unless you add `-v`.
How do I use a custom Dockerfile instead of a prebuilt image?
Replace `image:` with a `build:` block pointing to the directory containing your Dockerfile, e.g. `build: ./app`. Run `docker compose up -d --build` to rebuild when the Dockerfile changes.
Can I use Docker Compose in production?
Yes for single-host deployments. For multi-host orchestration or high-availability workloads, consider Docker Swarm or Kubernetes. Compose files are compatible with Swarm stacks via `docker stack deploy`.
Why does my app container start before the database is ready even with depends_on?
`depends_on` with the default `service_started` condition only waits for the container process to launch, not for the application inside it to be ready. Add a `healthcheck` to the database service and set `condition: service_healthy` on the dependency.
How do I pass different configurations for development vs production?
Use a base `compose.yaml` and override files such as `compose.override.yaml` for dev and `compose.prod.yaml` for production. Merge them at runtime: `docker compose -f compose.yaml -f compose.prod.yaml up -d`.

Related guides