# Docker Compose commands cheat sheet

> Docker Compose commands by task: start, stop, logs, exec, run, pull, build, config, profiles, watch, plus the flags that bite, such as down -v.

A reference for the `docker compose` commands you use on a server, grouped by task. Each table lists the command and what it does, followed by the details that cause trouble in practice. Everything here is for Compose v2 and later (the `docker compose` plugin). If you still type `docker-compose`, see [Docker Compose on Ubuntu](https://datapasa.com/wiki/server-management/docker-compose-on-ubuntu#migrate-from-docker-compose-v1) for the switch. If Docker is not installed yet, start with [install Docker on Ubuntu](https://datapasa.com/wiki/server-management/install-docker-on-ubuntu).

## How Compose finds your project

Most commands work on "the current project" without naming it. Compose works it out like this:

- **The file.** Without `-f`, Compose looks for `compose.yaml` (or `compose.yml`, `docker-compose.yaml`, `docker-compose.yml`) in the current folder, then in parent folders. If `compose.override.yaml` sits next to it, that file is merged on top automatically.
- **The project name.** Taken from `-p`, then the `COMPOSE_PROJECT_NAME` variable, then a top level `name:` in the file, and finally the folder name. It prefixes all containers, networks and volumes, so `/opt/blog/compose.yaml` creates `blog-web-1`, `blog_default` and so on.
- **Variables.** Values for `${VAR}` in the file come from your shell and from `.env` in the project folder.

Practical rule: `cd` into the project folder before you run anything. Most "my containers are gone" moments are commands run from the wrong folder, which Compose treats as a different, empty project.

## Start and stop

| Command | What it does |
|---|---|
| `docker compose up -d` | Create and start all services in the background |
| `docker compose up -d app` | Start only `app` (and what it depends on) |
| `docker compose up -d --wait` | Start, then wait until services are running or healthy |
| `docker compose stop` | Stop containers, keep them |
| `docker compose start` | Start containers that were stopped |
| `docker compose restart app` | Stop and start `app` with its existing configuration |
| `docker compose down` | Stop and remove containers and the project network |
| `docker compose pause` / `unpause` | Freeze and resume processes without stopping them |
| `docker compose kill` | Stop immediately with SIGKILL, no graceful shutdown |

Gotchas:

- **`restart` does not apply changes.** After editing `compose.yaml` or `.env`, run `docker compose up -d`. It recreates only the containers whose configuration or image changed and leaves the rest running.
- **`up` without `-d`** attaches your terminal to the logs, and Ctrl+C stops the whole project. On a server you almost always want `-d`.
- **`stop` vs `down`.** `stop` keeps the containers and is quick to undo with `start`. `down` removes them. Data in named volumes survives both.
- **Slow shutdown.** Containers get 10 seconds to exit before they are killed. Databases sometimes need more: `docker compose stop -t 60` or `stop_grace_period:` in the file.

## Status and logs

| Command | What it does |
|---|---|
| `docker compose ps` | Running containers of this project, with status and ports |
| `docker compose ps -a` | Include stopped and exited containers |
| `docker compose ls` | All running Compose projects on the server |
| `docker compose ls -a` | All projects, including stopped ones |
| `docker compose logs -f` | Follow the logs of all services |
| `docker compose logs -f --tail 100 app` | Last 100 lines of `app`, then follow |
| `docker compose logs --since 30m` | Only the last 30 minutes |
| `docker compose logs -t` | Add timestamps to each line |
| `docker compose top` | Processes running inside each container |
| `docker compose stats` | Live CPU and memory usage per container |
| `docker compose port app 8080` | Which host address a container port is published on |

Gotchas:

- **`logs` without `--tail` prints everything** since the container was created. On a container that has run for months this floods the terminal. Make `--tail 100` a habit.
- **`docker compose ls`** is the fastest way to find where a project lives: the `CONFIG FILES` column shows the path of its `compose.yaml`.

## Run commands inside containers

| Command | What it does |
|---|---|
| `docker compose exec app sh` | Shell in the running `app` container |
| `docker compose exec -u root app sh` | Same, as root |
| `docker compose exec db psql -U postgres` | Run a tool inside a service |
| `docker compose exec -T db pg_dump -U postgres mydb > dump.sql` | Pipe output to a file on the host |
| `docker compose run --rm app sh` | Start a new one off container from the `app` service |
| `docker compose run --rm --no-deps app ./migrate` | One off task without starting dependencies |
| `docker compose cp app:/app/config.yml ./config.yml` | Copy a file from a container to the host |
| `docker compose cp ./config.yml app:/app/config.yml` | Copy a file from the host into a container |

Gotchas:

- **`exec` vs `run`.** `exec` enters a container that is already running. `run` creates a fresh container from the service definition, which is useful for migrations and maintenance scripts, or when the service is stopped.
- **Always add `--rm` to `run`.** Without it, every run leaves a stopped container behind.
- **`run` does not publish ports** from the service definition. Add `--service-ports` if you need them.
- **Use `-T` when piping** in or out (`> dump.sql`, `< dump.sql`), and in cron jobs. Without it, the allocated TTY can add carriage returns to the output or fail when there is no terminal.
- **`bash` may not exist.** Many images are based on Alpine or are minimal. Try `sh` if `bash` is not found.

## Images and builds

| Command | What it does |
|---|---|
| `docker compose pull` | Download newer versions of all images |
| `docker compose pull app` | Only for one service |
| `docker compose images` | Images used by the project's containers |
| `docker compose build` | Build services that have a `build:` section |
| `docker compose build --no-cache` | Build from scratch, ignoring the layer cache |
| `docker compose build --pull` | Fetch a newer base image before building |
| `docker compose up -d --build` | Rebuild, then start |
| `docker compose up -d --force-recreate` | Recreate containers even if nothing changed |
| `docker compose up -d --no-deps app` | Recreate `app` without touching its dependencies |
| `docker compose up -d --pull always` | Pull images on every `up` |

The standard update routine for a project built from published images:

```bash
docker compose pull
docker compose up -d
docker image prune -f
```

Gotchas:

- **`pull` alone changes nothing** for running containers. They keep the old image until `up -d` recreates them.
- **`up` does not rebuild** an image that already exists. After changing a Dockerfile or source code, use `up -d --build`.
- **`--force-recreate`** helps when a container needs a clean start, for example after changing log settings in `daemon.json`. It does not pull or rebuild anything.

## Check the configuration

| Command | What it does |
|---|---|
| `docker compose config` | Print the final configuration with variables filled in and files merged |
| `docker compose config --quiet` | Only validate, print nothing on success |
| `docker compose config --services` | List service names |
| `docker compose config --volumes` | List named volumes |
| `docker compose config --images` | List images the project uses |
| `docker compose config --environment` | Show the variables used for substitution |
| `docker compose --dry-run up -d` | Show what `up` would do without doing it |

Gotchas:

- **`config` prints secrets.** Passwords from `.env` appear in plain text in its output. Do not paste it into chats or tickets unedited.
- **Run `config --quiet` before every `up`** after editing YAML. Indentation errors show up with a line number instead of a half started project.

## Several files and projects

| Command | What it does |
|---|---|
| `docker compose -f compose.yaml -f compose.prod.yaml up -d` | Merge files, later ones override earlier ones |
| `docker compose -f /opt/blog/compose.yaml ps` | Work on a project from any folder |
| `docker compose -p staging up -d` | Run a second copy of the project under another name |
| `docker compose --env-file .env.prod up -d` | Use another variables file |
| `docker compose --project-directory /opt/blog ps` | Set the base folder for relative paths |

Gotchas:

- **Repeat the same `-f` and `-p` flags on every command.** If you started with `-p staging`, a plain `docker compose down` targets a different project and does not stop staging.
- **Setting them once.** `COMPOSE_FILE`, `COMPOSE_PROJECT_NAME` and `COMPOSE_PROFILES` can go into the project's `.env` file, so the short commands keep working. Separate several files in `COMPOSE_FILE` with `:` on Linux.
- **Relative paths** in all files are resolved from the folder of the first file, not of the file they appear in.

## Profiles

Profiles mark optional services that do not start by default, for example an admin tool or a one off backup job:

```yaml
services:
  db:
    image: postgres:18
  adminer:
    image: adminer
    profiles: [tools]
    ports:
      - "127.0.0.1:8081:8080"
```

| Command | What it does |
|---|---|
| `docker compose up -d` | Starts only services without a profile (`db`) |
| `docker compose --profile tools up -d` | Also starts services in the `tools` profile |
| `COMPOSE_PROFILES=tools docker compose up -d` | Same, with an environment variable |
| `docker compose --profile "*" up -d` | Enable every profile |
| `docker compose up -d adminer` | Naming a service starts it even if its profile is off |
| `docker compose --profile tools down` | Remove profiled services together with the rest |

Gotcha: a plain `docker compose down` ignores services in inactive profiles. Their containers keep running. Pass the same `--profile` to `down` that you used for `up`.

## Watch mode (development)

`watch` rebuilds or updates containers automatically when files on disk change. It works only for services with a `build:` section and a `develop.watch` block:

```yaml
services:
  web:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json
```

| Command | What it does |
|---|---|
| `docker compose watch` | Start watching, show sync and rebuild events |
| `docker compose up --watch` | Start the project with watching and normal logs |

The actions are `sync` (copy changed files into the container), `rebuild` (build a new image and replace the container) and `sync+restart` (copy, then restart). Watch mode is meant for development. On a production server, deploy deliberately with `pull` or `build` and `up -d`.

## Clean up, and the dangerous flags

| Command | What it does | Data loss? |
|---|---|---|
| `docker compose down` | Remove containers and project networks | No, volumes stay |
| `docker compose down --remove-orphans` | Also remove containers of services deleted from the file | No |
| `docker compose up -d --remove-orphans` | Same cleanup while starting | No |
| `docker compose rm` | Remove stopped service containers | No |
| `docker compose down --rmi all` | Also remove the project's images | No, but they must be pulled again |
| `docker compose down -v` | Also remove named and anonymous volumes | **Yes** |
| `docker compose up -d -V` | Recreate anonymous volumes instead of reusing them | Yes, for anonymous volumes |

> **docker compose down -v deletes your data.** `-v` removes the named volumes declared in the file. For a database that means the whole database, with no undo. It is useful when you deliberately want to start over in development. On a server, use plain `down`, and have a [backup](https://datapasa.com/wiki/server-management/back-up-your-vps) before you ever type `-v`.

Gotchas:

- **Orphans.** When you rename or delete a service in the file, its old container keeps running and Compose warns about `orphan containers`. `--remove-orphans` removes them.
- **External volumes and networks** (marked `external: true`) are never removed by `down`, not even with `-v`.
- **Server wide cleanup** is done with plain Docker commands, not Compose: `docker image prune` for unused images, `docker system df` to see what takes space. Be careful with `docker volume prune --all`: it deletes every volume no container uses at that moment, including the data of projects that are currently down. Without `--all`, current Docker versions only remove unused anonymous volumes.

## Coming from docker-compose v1

| Old command | New command |
|---|---|
| `docker-compose up -d` | `docker compose up -d` |
| `docker-compose logs -f web` | `docker compose logs -f web` |
| `docker-compose exec web sh` | `docker compose exec web sh` |
| `docker-compose -f prod.yml up -d` | `docker compose -f prod.yml up -d` |

Subcommands and most flags kept their names. The visible differences are container names with hyphens (`project-web-1` instead of `project_web_1`) and the `version:` line at the top of files, which is now ignored.

## Related guides

- [Docker Compose on Ubuntu](https://datapasa.com/wiki/server-management/docker-compose-on-ubuntu): install Compose v2 and build an app plus PostgreSQL project step by step.
- [Install Docker on Ubuntu](https://datapasa.com/wiki/server-management/install-docker-on-ubuntu): Docker Engine setup and troubleshooting.
- [Back up your VPS](https://datapasa.com/wiki/server-management/back-up-your-vps): protect volumes before updates and cleanups.
