Debugging a Compose project starts with the docker compose ps command, which shows the status of every service, and the docker compose logs command, which prints container output. To follow logs in real time, add -f; to scope to one service, pass its name: docker compose logs -f web; to scope to multiple services, list them: docker compose logs -f web db. To limit output to recent lines, use --tail=100, and to add timestamps, use --timestamps. For a quick health check, docker compose top shows running processes inside each container, and docker compose stats is a Compose v2 shortcut for live CPU and memory usage. To watch lifecycle events like start, die, and kill, useful when a container keeps restarting, use docker compose events.
To interact with a running container, use docker compose exec with a command. docker compose exec web sh opens a shell, docker compose exec web env | sort lists all environment variables inside the container (a great way to verify what actually got set), and docker compose exec --user root web sh escalates to root inside a container that normally runs as a non-root user. To copy files in or out, use docker compose cp web:/path ./local and the reverse form. To see which host port maps to a container port, use docker compose port web 80. To run a one-off command without modifying the compose file, use docker compose run --rm web bash, remembering that the service name must match a defined service and the working directory must contain a compose file, or you need to pass -f.
Several pitfalls recur often enough to be worth memorizing. The "service not found" error on docker compose run is almost always a typo or a wrong working directory; Compose only loads docker-compose.yml from the current directory unless you point it elsewhere. Port conflicts happen when another container, another project, or a host process binds the same port; the fix is to change the host-side port in the ports: mapping, e.g. '8081:80', or to find the offending process with sudo lsof -iTCP:8080 -sTCP:LISTEN or ss -ltn. Bind mounts sometimes appear empty inside the container because the host path is wrong or the relative path was resolved from the wrong directory; remember that relative paths in volumes: are resolved from the directory containing the compose file, not the directory you ran the command from. On macOS and WSL, file-watching via inotify often fails over bind mounts, so hot-reload tools may need polling mode. On WSL2 in particular, keeping your project under the Linux filesystem rather than /mnt/c/ dramatically improves performance. To clean up, docker compose down removes containers and networks but keeps volumes, while docker compose down -v --remove-orphans also removes volumes and stray containers from removed service definitions. For nuclear cleanup, docker system prune -a --volumes removes all unreferenced images, networks, and volumes; treat it as a last resort.