Docker Compose cheat sheet for real servers
The Compose commands you reach for every day, grouped by job: lifecycle, applying changes, logs, shells, networks, volumes, and cleanup that is safe.
The Compose commands you actually reach for
Docker Compose ships dozens of subcommands. Daily work on a server uses about a dozen. This page groups those by the job you are doing, gives one plain reason for each, and points at the deep dive when a command hides a trap.
Everything here uses Compose V2 or V5: docker compose with a space, not the old docker-compose script. Both are Go plugins; on Linux, the Compose plugin is installed separately from Docker Engine. Ubuntu 24.04 LTS still offers the legacy Python V1 tool as docker-compose version 1.29.2-6ubuntu1 in its universe repository. Check with docker compose version. If the command is unavailable, set up Docker's repository and install the docker-compose-plugin package.
Every command below runs from the directory that holds your compose.yaml; Compose uses that directory's name as the default project name unless you override it. Without -f or COMPOSE_FILE, Compose searches the working directory and its parents for a configuration file, so running one level up only finds your stack if its file is still on that search path. If the file format itself is new to you, start with a first Compose file on a VPS and come back here for the commands.
Lifecycle: the four you type, and the one that removes containers
docker compose up -d
docker compose up -d --wait
docker compose stop
docker compose start
docker compose restart web
docker compose downup -d creates the network, creates the containers, starts them, and returns. It returns after starting the containers without waiting for application readiness, which is why a deploy script that follows it with a curl probe often fails on the first try. up -d --wait waits for services with healthchecks to become healthy and services without them to be running; an unhealthy service or an expired --wait-timeout causes a non-zero exit. The flag is only as good as the check behind it, so write a healthcheck Compose can trust before you lean on it in automation.
stop halts the containers and keeps them, so start brings the same containers back with the same writable layer. down stops them and then removes the containers and the project network. Anything written inside the container and outside a volume goes with them. That is the most expensive misunderstanding in Compose, and the full difference between down and stop covers where it bites.
restart is not a reload. It stops and starts the same container with the configuration it already has, so a changed environment variable, a new image tag or an edited port mapping has no effect at all. To apply a file change you run up -d again. Compose compares each service against its running container and recreates the ones whose configuration or image changed.
Applying a change: recreate, pull, or rebuild
docker compose up -d --force-recreate
docker compose pull && docker compose up -d
docker compose build --no-cache web
docker compose up -d --build webup -d reuses running containers when their configuration and image are unchanged; image pulls and builds follow the service's pull policy. --force-recreate overrides that comparison and replaces every container even when the configuration is identical, so it is the quickest way to clear odd in-container state.
The explicit image update routine uses two commands because they do two different things. pull downloads the current image for each tag named in the file. up -d then sees that the service's image ID no longer matches its running container and recreates it. With the default missing pull policy, cached tags are reused except for latest, which is always pulled. The opposite risk shows up on a multi-service stack, where pulling latest for every service at once can break an app that was working ten seconds earlier, which is why a self-hosted AFFiNE workspace recommends pinning the application and migration image to the same digest instead. Pinning also turns an upgrade into a deliberate edit of the tag followed by that same pull and recreate, and on a stack that migrates its database as it boots you want a dump in hand before either command, which is the routine a self-hosted Chatwoot support desk follows for every version bump.
build applies to services that declare a build: section, including services that also declare an image:. up -d --build builds and starts in one step, which is the normal loop while you are changing code. Reach for --no-cache only when a cached layer is clearly stale, because it rebuilds every layer from scratch. Some projects ship both routes from one cloned repo, and then the two loops are alternative update paths: a self-hosted openGym workout tracker uses docker compose up -d --build to build its pinned checkout, because pulling its latest images can run a newer release than that checkout.
Seeing what is running
docker compose ps
docker compose ps -a
docker compose logs -f --tail=100
docker compose logs --since 15m --timestamps db
docker compose top
docker compose lsps lists running containers only. A service that crashed during start is invisible there until you add -a, so a container missing from ps while ps -a shows it as Exited (1) is the normal shape of a startup failure. Read the exit code, then read the logs.
logs -f follows every service at once and prefixes each line with the service name, which is the view you want when services talk to each other and the order of events matters. Name a service to narrow it. --tail=100 matters on a container that has been up for a month, because the default prints the entire history and floods the terminal. --since 15m answers the question you usually have, which is what happened during the restart you just did.
top lists the processes inside each container, which separates "the container is running" from "the process inside it is running". ls steps outside the current directory and lists running Compose projects on the host with their status; add -a to include stopped projects.
Getting a shell inside a service
docker compose exec web sh
docker compose exec -u root web sh
docker compose run --rm web env
docker compose run --rm --no-deps web shexec runs a command inside a container that is already up. run starts a new container from the same service definition, which is what you need when the service does not stay up long enough to exec into. Always pair run with --rm, because without it every invocation leaves a stopped container behind, and those pile up until docker compose ps -a is unreadable.
Try sh before bash. Some Alpine based images include bash, including the official PostgreSQL Alpine image; when it is absent, the failure reads exec: "bash": executable file not found in $PATH. Adding --no-deps to run skips the service's dependencies, which stops a quick config check from booting your whole database.
run --rm web env is the fastest way to see the environment a service really got, after every .env file, environment: block and shell variable has been merged. When a value is wrong, the merge order is usually the reason, and how Compose resolves env files and secrets sets out which source wins.
Networks, ports, and name resolution
docker compose port web 80
docker compose exec web getent hosts db
docker compose config --networksBy default, Compose puts every service on one project network, and each service name is a DNS name on it. Running getent hosts db inside web, when the image includes getent, checks name resolution; it does not test whether db accepts connections. If the name resolves but the connection is refused, check that the server is running and listening on the requested address and port; binding only to 127.0.0.1 is one possible cause. The same boundary explains why a container started outside the project, whether by docker run or as its own stack, needs a shared network to resolve a service name like jellyfin, which is the first thing to check when a Halcyon front end for your Jellyfin library cannot reach the server it points at. The rest of that model is in how Compose networks and service DNS work.
port web 80 prints the host address and port a container port is published on, which saves guessing when the mapping came from a variable. Publishing a port also writes a firewall rule that Docker manages itself, and that rule sits ahead of yours, so a service you believed was private can be open to the internet. That case is covered in why published Docker ports bypass ufw. Leaving those ports unpublished and putting one authenticating proxy on the project network in front of the services instead is the safer shape, which is what running Authentik as a single sign-on layer gives you.
Volumes and data
docker compose config --volumes
docker compose cp db:/etc/postgresql/pg_hba.conf ./pg_hba.conf
docker compose down -vconfig --volumes prints the named volumes the project declares, one per line. That list is only part of a backup inventory: bind mounts and configuration files also need checking. When the volumes hold something irreplaceable, the exact backup command matters as much as the list, which is why the PhotoPrism and Immich comparison spells out the dump and copy commands each photo server needs. cp copies a file into or out of a container without opening a shell, using the service:path form on whichever side is the container.
down -v removes declared named volumes except external ones, plus anonymous volumes attached to the containers, along with the containers. It is the right command for tearing down a test stack and the wrong one for anything holding data you care about, because there is no confirmation and no undo. Bind mounts survive it, since they live on the host filesystem. That gap in blast radius is one reason to choose deliberately between bind mounts and named volumes.
Cleanup that frees disk without losing data
docker compose down --remove-orphans
docker system df
docker image prune -a
docker builder prune--remove-orphans deletes containers that belong to the project but no longer appear in the file, which is exactly what you get after renaming a service. Without it those containers keep running, and docker compose ps includes running orphans by default.
docker system df shows where the disk went before you delete anything, splitting images, containers, local volumes and build cache with a reclaimable figure for each. image prune -a removes every image no container uses, tagged or not, and on a box that has pulled several versions of a large image that is usually the biggest win. builder prune removes dangling build cache by default; add -a to remove all unused build cache.
None of those touch a named volume. docker volume prune --all, docker compose down -v and docker volume rm remove named volumes. Plain docker volume prune removes anonymous volumes only.
Checking the file before it breaks something
docker compose config --quiet
docker compose config --services
docker compose --dry-run up -dconfig --quiet validates and prints nothing on success, so it belongs in a pre-deploy step or a git hook. Plain config prints the fully merged and interpolated file, which is how you confirm a variable resolved and an override file layered the way you expected. An unset variable appears there as an empty value, next to the warning The "X" variable is not set. Defaulting to a blank string.
--dry-run is a global flag rather than a subcommand flag, so it goes before up. It prints every action Compose would take and changes nothing, which is thirty seconds well spent before a down on a stack that matters.
Working across files, profiles, and projects
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose --profile debug up -d
docker compose -p staging up -dMultiple -f flags merge in order, and later files override earlier ones key by key. That is the standard way to keep one base file with a small production override, though the rules differ for lists and maps, so read how Compose merges multiple files before you debug a surprise.
--profile starts services tagged with that profile alongside the untagged ones, which keeps debug tooling out of a normal up. -p sets the project name, so two copies of one stack can run side by side with separate networks and separate volume names. Getting the stack back after a reboot uses a restart policy or a unit that runs Compose for you, described in starting Compose stacks on boot.
FAQ
What replaced docker-compose with a hyphen?
Compose V2 and V5, invoked as docker compose with a space. On Linux, Compose is a separately installed plugin; Ubuntu 24.04 LTS still offers the V1 Python tool in its universe repository. If the space form is unavailable, set up Docker's repository and install the docker-compose-plugin package. Update old scripts to the space form rather than adding an alias, because V2 has flags V1 never had.
Why does docker compose restart not pick up my config change?
restart stops and starts the existing container with the configuration it was created with, and it never re-reads compose.yaml. Any change to environment variables, ports, volumes or the image tag needs docker compose up -d, which compares each service against its running container and recreates the ones that differ. Add --force-recreate when you want the replacement to happen even though nothing in the file changed.
How do I update a service to a newer image?
Run docker compose pull, then docker compose up -d. The pull fetches the current image for each tag in the file, and up -d recreates any service whose image ID no longer matches its container. Running up -d on its own follows the pull policy: the default missing policy reuses cached images except for latest, which is always pulled.
Which cleanup commands are safe on a live server?
docker system df reports disk usage without deleting anything; docker image prune -a and docker builder prune remove unused images and build cache, so running services keep working and named volumes are untouched. The dangerous pair is docker compose down -v, which deletes the project's declared non-external named volumes and attached anonymous volumes with no prompt, and docker volume prune --all, which deletes unused local volumes, including named ones, after a single y/N prompt, or none at all with -f. Run docker compose config --volumes first so you know what is at risk.
Can I run one command without starting the whole stack?
Yes. docker compose run --rm --no-deps web sh starts a single container from the web service definition, skips its dependencies, and removes the container when you exit. Use exec instead when the container is already running, because exec runs your command inside the live container and shows you the state the service is actually in.