← Back to Blog DevOps

Docker Run vs Docker Compose: When to Use Each

The containerization ecosystem has revolutionized how developers build, ship, and run applications. At the core of this revolution is Docker, a platform that enables isolated execution environments. When interacting with Docker, you typically use either the imperative command-line interface via docker run or the declarative approach via Docker Compose.

While both tools ultimately accomplish the same goal—running containers—their use cases, syntax, and long-term scalability differ significantly. docker run launches a single container from the command line, whereas Docker Compose defines multi-container applications in a structured YAML file, making complex deployments reproducible and version-controlled.

In this comprehensive guide, we'll dive deep into the technical nuances of both approaches. We'll explore when to use each, how their networking models differ, how to handle state management, and how to seamlessly transition from complex CLI commands to elegant YAML configuration. If you already have a working docker run command and need to convert it to a docker-compose.yml file instantly, our Docker Run to Compose Converter handles the translation locally in your browser, ensuring your server configurations stay completely private.

Deep Dive: Docker Run

The docker run command is the foundation of container execution. It creates a writeable container layer over the specified image, and then starts it using the specified command. It is the most direct, imperative way to launch a container. You tell Docker exactly what to do, step by step, right in your shell.

Anatomy of a Docker Run Command

Let's look at a common scenario: running a PostgreSQL database for local development. A robust docker run command might look like this:

docker run -d \
  --name postgres-dev \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_USER=admin \
  -e POSTGRES_PASSWORD=secret123 \
  -e POSTGRES_DB=myapp \
  --restart unless-stopped \
  postgres:16-alpine

Breaking down the flags used in this command:

  • -d (or --detach): Runs the container in the background and prints the new container ID. Without this, your terminal would be locked to the container's standard output.
  • --name: Assigns a human-readable name (postgres-dev) to the container. If omitted, Docker generates a random, often amusing string (like nervous_wu).
  • -p (or --publish): Maps a port on the host machine to a port inside the container (host_port:container_port). Here, we expose PostgreSQL's default port 5432 to the host.
  • -v (or --volume): Mounts a host directory or named volume into the container to persist data. If the container is destroyed, the data in pgdata remains intact on the host.
  • -e (or --env): Passes environment variables into the container. These are crucial for configuring applications, setting passwords, or defining operational modes.
  • --restart: Defines the container's restart policy. unless-stopped ensures the database restarts if it crashes or if the Docker daemon reboots, unless you explicitly stop it.
  • postgres:16-alpine: The image to run, specified in repository:tag format. The Alpine variant is chosen here for its minimal footprint.

Expected Output

When you execute this command successfully, Docker simply outputs the full container ID hash:

$ docker run -d --name postgres-dev -p 5432:5432 ... postgres:16-alpine
f9b2b2a1a8c9b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8

When to Use Docker Run

Despite its imperative nature, docker run shines in specific scenarios:

  • Quick Tests and Experiments: When you want to spin up a quick Python environment (docker run -it python:3.11 bash) to test a script without polluting your host machine.
  • Utility Scripts and Cron Jobs: For isolated tasks, such as running a one-off database backup script or executing an AWS CLI command via container.
  • CI/CD Pipeline Steps: Sometimes a pipeline needs a single, ephemeral container to run a linter or test suite before being destroyed immediately after.

However, the biggest drawback of docker run is state retention. The command exists only in your bash history. If you accidentally clear your history or move to a new machine, that exact configuration is lost unless you saved it in a shell script. Additionally, managing dependencies (like starting a database before starting an API) becomes an exercise in manual scripting.

Deep Dive: Docker Compose

Docker Compose takes the exact same configuration parameters used in docker run and defines them declaratively within a docker-compose.yml file. Instead of telling Docker how to run the container flag by flag, you tell Docker what the final state should look like, and Compose handles the execution.

Anatomy of a docker-compose.yml File

Let's translate our previous PostgreSQL command into a Compose file:

version: "3.8"

services:
  postgres:
    image: postgres:16-alpine
    container_name: postgres-dev
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret123
      POSTGRES_DB: myapp
    restart: unless-stopped

volumes:
  pgdata:

This YAML structure perfectly mirrors our CLI flags but offers immense advantages. The configuration is now version-controlled, shareable, and reproducible. You can commit this file to Git, and any developer on your team can bring up the exact same database environment with a single command:

$ docker compose up -d
[+] Running 2/2
 ✔ Volume "myapp_pgdata"      Created
 ✔ Container postgres-dev     Started

When you are done, tearing down the environment is just as easy, and it automatically cleans up the networks and containers (while preserving named volumes):

$ docker compose down
[+] Running 2/2
 ✔ Container postgres-dev     Removed
 ✔ Network myapp_default      Removed

Orchestrating Multi-Container Apps

The true power of Compose is unleashed when your application requires multiple services. Imagine a web application consisting of a Node.js backend, a React frontend, a Redis cache, and a PostgreSQL database. Managing four separate docker run commands, ensuring they start in the right order, and manually linking them on a custom Docker network is tedious.

With Compose, you simply add more blocks under the services: key. Compose automatically creates a dedicated bridge network for your project. All services within this Compose file can communicate with each other using their service names as hostnames (e.g., the Node API can connect to the database simply by using the hostname postgres).

Docker Run vs Compose: Side-by-Side Comparison

When deciding between command-line execution and declarative YAML, comparing their operational characteristics helps explain why workflows naturally transition to Compose as projects scale.

Feature / Scenario docker run (Imperative) Docker Compose (Declarative)
Configuration Storage Shell history, bash scripts, or aliases. A declarative YAML file (docker-compose.yml).
Multi-Container Apps Requires complex manual networking, linking, and bash scripting. Native orchestration with automatic, isolated networking.
Version Control & CI/CD Difficult to version-control effectively; harder to share across teams. Easily checked into Git for reproducible, standardized environments.
Startup & Teardown Multiple manual commands needed per container (start, stop, rm). Single command lifecycle (docker compose up / down).
Networking Containers attach to the default bridge unless manually specified. Automatically creates a custom project network with DNS resolution.
Best Used For Quick tests, isolated scripts, single tasks, utility commands. Local dev stacks, team collaboration, staging/production infrastructure.

Translating CLI Flags to Compose Directives

If you are transitioning from imperative commands to a declarative workflow, you need to know how CLI flags map to YAML keys. Almost every docker run flag has a corresponding Compose directive:

  • -p 8080:80 translates to the ports: array (- "8080:80").
  • -v data:/app/data translates to the volumes: array (- data:/app/data").
  • -e KEY=value translates to the environment: dictionary (KEY: value).
  • --name myservice translates to container_name: myservice.
  • --network mynet translates to the networks: array.
  • --restart always translates to restart: always.
  • -d (detach) is implied in Compose workflows when you run docker compose up -d.

For complex commands with many flags, manual conversion is tedious and error-prone. One missed space can break the YAML syntax. To save time, use the Docker Run to Compose Converter. It parses your complex CLI command, maps every flag to the correct YAML structure, and outputs a valid docker-compose.yml file instantly.

Managing Environment Variables and Secrets

In a docker run command, environment variables are passed explicitly with the -e flag. This often results in massive command lines exposing sensitive API keys and database passwords in plain text in your shell history.

Hardcoding these secrets in your docker-compose.yml is equally risky if the file is committed to version control. The industry standard approach with Docker Compose is utilizing the env_file directive alongside a .env file:

services:
  api:
    image: myapp-backend:latest
    env_file:
      - .env
    ports:
      - "3000:3000"
    depends_on:
      - database

This instructs Compose to load all variables from a .env file located in the same directory. You then add .env to your .gitignore file to ensure secrets never touch your Git repository.

If you find that your .env file has grown messy over time with duplicate keys, trailing whitespace, or inconsistent formatting, you can clean it up instantly using our local Env File Formatter tool.

Troubleshooting Common Conversion & Deployment Issues

Whether transitioning from standalone commands to declarative YAML or debugging complex multi-container stacks, developers frequently encounter configuration and permission obstacles. Here are practical troubleshooting workflows for the most common deployment issues.

1. Volume Permission Denied (EACCES)

Symptom: Your container starts but immediately crashes with 13: Permission denied, or your application fails to write logs to a bound host directory.

Cause: When mounting host directories into containers (such as PostgreSQL data directories or Node.js workspaces), file ownership mismatches occur between the host user's UID and the container's internal UID.

Solution: To diagnose and fix UID/GID mapping errors without guessing Linux chmod/chown flags, use our Docker Volume Permissions Helper. It generates the exact volume permission fixes and user directives needed for your specific environment.

2. YAML Schema & Indentation Errors

Symptom: Running docker compose up yields an error like yaml: line 12: did not find expected key or it fails silently and ignores certain configurations.

Cause: YAML is strictly whitespace-sensitive. A single misplaced space, mixed tabs and spaces, or unquoted strings containing special characters (like : or # in a password) will break parsing.

Solution: Always use spaces (not tabs) for indentation. Before deploying, validate your YAML structure with the YAML Validator to catch syntax errors, or use our dedicated Docker Compose Validator to verify schema compliance and detect duplicate port mappings right in your browser.

3. Port Allocation Conflicts

Symptom: You see Error starting userland proxy: bind: address already in use when launching a container.

Cause: Another process (or another Docker container) on your host machine is already bound to the port you are trying to expose.

Solution: Use docker ps to see if another container is using the port, or use netstat -tuln (on Linux) to check host processes. Resolve it by mapping to a different host port in your Compose file (e.g., change "80:80" to "8080:80").

Best Practices for Docker Compose

To get the most out of Docker Compose in professional environments, adhere to these standard best practices:

  • Use relative paths for volumes: Instead of hardcoding /Users/name/project/data, use ./data:/app/data. This ensures the Compose file works on any developer's machine.
  • Leverage the depends_on directive: Use depends_on to dictate startup order (e.g., ensuring the database starts before the backend API). However, note that Compose only waits for the container to start, not for the service inside to be "ready". Implement healthchecks or retry logic in your application code.
  • Name your resources intentionally: While Compose auto-generates names for volumes and networks based on the directory name, explicitly naming them helps avoid conflicts when running multiple instances of a project.
  • Avoid using container_name unless necessary: Specifying a hardcoded container_name prevents Compose from scaling the service (e.g., via docker compose up --scale web=3), as container names must be unique.

Frequently Asked Questions

Can I convert any docker run command to docker-compose.yml?
Most docker run flags have direct docker-compose.yml equivalents: -p maps to ports, -v maps to volumes, -e maps to environment, --name maps to the service name, and --network maps to networks. Complex flags like --cap-add or --device may require additional configuration block structures in the Compose file.
When should I absolutely use Docker Run instead of Compose?
Use docker run for quick, one-off tasks. If you need to test a specific image build, run a temporary database for a quick test script, execute a utility container (like a curl command), or run a temporary interactive shell, docker run is much faster than creating a YAML file.
Does Docker Compose work with Docker Swarm or Kubernetes?
Docker Compose files can be used natively with Docker Swarm via the docker stack deploy command. However, some Compose features (like the build directive) are ignored in Swarm mode. For Kubernetes, you typically use tools like Kompose to translate docker-compose.yml files into Kubernetes deployment manifests.
How do I override configurations for different environments?
You can use multiple Compose files. By default, Compose reads docker-compose.yml and an optional docker-compose.override.yml. You can also specify multiple files via the CLI: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d. The latter file merges with and overrides configurations from the former.
Is docker-compose deprecated in favor of docker compose?
Yes, the standalone docker-compose binary (Python-based, Compose V1) was officially deprecated in July 2023. Docker Compose V2 is written in Go and is integrated directly into the Docker CLI as a plugin, accessed via docker compose (without the hyphen). The YAML Compose file format itself remains the standard and is fully supported.