DevToys Web Pro iconDevToys Web ProBlog
Przetłumaczono za pomocą LocalePack logoLocalePack
Oceń nas:
Wypróbuj rozszerzenie przeglądarki:
← Back to Blog

Docker Run to Compose: Flag Mapping, Override Files, and Pitfalls

9 min read

Documentation always shows the quick path: docker run -p 5432:5432 -v ./data:/var/lib/postgresql/data postgres:16. That works for a one-off test, but the moment you need a second service, share it with a teammate, or commit it to a repository, a single long command falls apart. Compose turns that command into a declarative YAML file that version control, CI, and every developer on the team can read and reproduce. Use the Docker Compose Converter to translate any docker run command instantly.

Flag-to-Compose Mapping

Every flag you pass to docker run has a direct equivalent in compose.yaml. The table below covers the flags you will encounter in almost every project:

docker run flagcompose.yaml fieldNotes
-p 8080:80ports: ["8080:80"]host:container; use string form to avoid YAML octal parsing of port 0x
-v ./data:/var/lib/datavolumes: ["./data:/var/lib/data"]Bind mount; relative paths resolve from the compose file location
-v myvolume:/var/lib/datavolumes: ["myvolume:/var/lib/data"] + top-level volumes: myvolume:Named volume; must be declared at the top-level volumes key
--network mynetnetworks: [mynet] + top-level networks: mynet:Custom network; declare at top level too
--restart unless-stoppedrestart: unless-stoppedValues: no, always, on-failure, unless-stopped
-e DB_USER=appenvironment: DB_USER: appOr list form: - DB_USER=app
--name postgrescontainer_name: postgresOmit in production — fixed names break scaling with --scale
--rmNo direct equivalentCompose removes containers on down; --rm is implicit for one-off run
-u 1000:1000user: "1000:1000"Quote the value — YAML parses bare numbers differently
--link app:appUse networks instead--link is deprecated; services on the same network resolve by service name
--health-cmd "pg_isready"healthcheck: test: ["CMD", "pg_isready"]Also set interval, timeout, retries

Before and After: Postgres + App

Here are two typical docker run commands that a README might show — one for Postgres, one for a web application that connects to it:

docker run -d \
  --name postgres \
  -p 5432:5432 \
  -e POSTGRES_USER=app \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=appdb \
  -v pgdata:/var/lib/postgresql/data \
  --restart unless-stopped \
  postgres:16-alpine

docker run -d \
  --name web \
  -p 3000:3000 \
  -e DATABASE_URL=postgres://app:secret@postgres:5432/appdb \
  --link postgres:postgres \
  --restart unless-stopped \
  myapp:latest

The equivalent compose.yaml:

services:
  postgres:
    image: postgres:16-alpine
    container_name: postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "app"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    image: myapp:latest
    container_name: web
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@postgres:5432/appdb
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  pgdata:

Notice that --link postgres:postgres is gone. Services on the same default Compose network reach each other by their service name (postgres) automatically. The depends_on with condition: service_healthy replaces the old pattern of sleeping before the web process starts.

Why Compose Beats Long Run Commands

  • Multi-service: one docker compose up starts everything in dependency order. No shell scripts with sequential docker run calls.
  • Reproducible: the file is the ground truth. New team members clone the repo and run one command.
  • Versioned: changes to ports, environment variables, and volume mounts are tracked in git with diffs and blame.
  • Fewer keystrokes: docker compose up -d instead of a 10-flag command you have to copy from a wiki page.
  • Readable: YAML with service names and comments is far clearer than a shell one-liner to a new hire.

Version Pinning

The single most common cause of "it worked last week" failures is image: postgres:latest. The :latest tag is a moving target — your CI pulls a different image than your laptop, and a major version bump silently breaks your schema migrations.

Pin to a specific minor version or patch:

# Avoid
image: postgres:latest

# Good — minor version pin
image: postgres:16-alpine

# Best — exact digest (for supply-chain security)
image: postgres:16.3-alpine3.20

A practical strategy: pin to the minor version in compose.yaml (e.g., postgres:16-alpine), and upgrade deliberately by bumping the tag and running your test suite. Use exact digest pins only in production or for security-sensitive images where you need full reproducibility.

Volumes: Bind Mount vs Named Volume

Two volume syntaxes look similar but behave differently:

services:
  db:
    image: postgres:16-alpine
    volumes:
      # Bind mount — maps a host path into the container
      # Good for: source code, config files you edit locally
      - ./data:/var/lib/postgresql/data

      # Named volume — Docker manages the storage location
      # Good for: database data, anything that should survive container rebuilds
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:  # must be declared here for named volumes

Use bind mounts for files you want to edit on the host and see reflected immediately inside the container — config files, source code in development. Use named volumes for database data and anything that needs to persist across docker compose down without living in a predictable host path. Named volumes survive down but are removed by docker compose down -v.

Networks

Compose creates a default bridge network for every project. All services in the same compose.yaml join it automatically and can resolve each other by service name. You do not need --link.

Custom networks are useful when you want to isolate communication or connect services across multiple compose files:

services:
  web:
    image: myapp:latest
    networks:
      - frontend
      - backend

  api:
    image: myapi:latest
    networks:
      - backend

  postgres:
    image: postgres:16-alpine
    networks:
      - backend

networks:
  frontend:
  backend:

Here web can reach both api and postgres, but postgres cannot initiate connections to web — it is not on the frontend network. This mirrors a real network security boundary without any firewall rules.

On macOS, host networking (network_mode: host) does not work the same way as on Linux. The Docker Desktop VM sits between your host and the containers, so host-networking bypasses the VM network, not your Mac's network. Use explicit port mappings instead.

Environment Variables

Three ways to supply environment variables in Compose, from simplest to most flexible:

services:
  web:
    image: myapp:latest

    # 1. Inline — fine for non-secret config
    environment:
      NODE_ENV: production
      PORT: "3000"

    # 2. env_file — keeps secrets out of compose.yaml
    env_file:
      - .env

    # 3. Interpolation from shell or .env file in project root
    environment:
      DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@postgres:5432/appdb

The .env file in the same directory as compose.yaml is automatically loaded for variable interpolation. This is different from env_file, which injects variables directly into the container environment. The distinction matters: interpolation variables are resolved at compose parse time; env_file variables are passed to the running container.

Never commit .env files containing real credentials. Add .env to your .gitignore and commit a .env.example with placeholder values.

Override Files

Compose merges multiple files when you pass them with -f. The canonical pattern uses a base file plus environment-specific overrides:

# compose.yaml — base, committed to git
services:
  web:
    image: myapp:latest
    ports:
      - "3000:3000"

  postgres:
    image: postgres:16-alpine
# compose.override.yaml — dev overrides, also committed
# Compose loads this automatically alongside compose.yaml
services:
  web:
    build: .          # build from source in dev instead of pulling
    volumes:
      - .:/app        # live reload: mount source into container
    environment:
      NODE_ENV: development
# compose.prod.yaml — production overrides, used with -f
services:
  web:
    environment:
      NODE_ENV: production
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure
# Development (compose.yaml + compose.override.yaml loaded automatically)
docker compose up -d

# Production
docker compose -f compose.yaml -f compose.prod.yaml up -d

# Staging
docker compose -f compose.yaml -f compose.staging.yaml up -d

This pattern keeps your dev setup (live source mounting, debug ports) completely separate from production without duplicating the entire service definition.

Compose v2 vs Compose v1

You may see two different commands in older documentation:

CommandVersionStatus
docker-compose upCompose v1 (Python)End-of-life since July 2023. Do not use.
docker compose upCompose v2 (Go plugin)Current. Bundled with Docker Desktop and Docker Engine.

The hyphen (docker-compose) is the legacy Python standalone binary. The space (docker compose) is the v2 plugin built into the Docker CLI. They read the same YAML format for most cases, but v2 added depends_on: condition: service_healthy, profiles, and the compose.yaml filename (v1 only looked for docker-compose.yml). Always use docker compose.

Pitfalls

  • docker compose up does not rebuild on code change. If you change your Dockerfile or application code, you must run docker compose up --build or docker compose build first. Otherwise Compose reuses the cached image.
  • restart: always vs restart: unless-stopped. With always, the container restarts even after you manually stop it with docker compose stop. Use unless-stopped so a deliberate stop is respected until the next daemon restart.
  • Host networking on macOS. network_mode: "host" does not give you host-level access on macOS or Windows — Docker Desktop runs containers inside a Linux VM. Bind the port explicitly with ports instead.
  • Secrets in compose.yaml. Hardcoded passwords in environment blocks end up in git history. Use env_file with a gitignored .env, or Docker Secrets for production Swarm deployments.
  • Named volumes not declared at the top level. Referencing a named volume in a service without declaring it under the top-level volumes: key causes a parse error. Bind mounts do not require this declaration.
  • depends_on does not wait for readiness by default. Without a healthcheck condition, depends_on only waits for the container to start, not for the database inside it to accept connections. Add a healthcheck to the dependency service and use condition: service_healthy.

Convert any docker run command to a complete compose.yaml with the Docker Compose Converter — paste your command and get valid YAML in one click, no account required.