Intro
Docker Compose local lab setup with practical examples helps operators move from an observed problem to a verified result. Start by identifying the installed Docker and Compose versions, deployment topology, prerequisites, and the exact component being inspected. A local lab that mirrors production enough to reproduce failures is essential for safe testing, debugging, and rollouts.
This article focuses on Docker Compose local lab for developers, DevOps consultants, and technical startup teams. It connects Docker Compose setup, testing, examples, and development to commands, expected output, failure signals, and recovery decisions. The goal is operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document how to recover if the expected state is not reached.
All examples use Docker Compose v2 syntax (docker compose without a hyphen). If you still have the legacy v1 command (docker-compose), upgrade to a current Docker Engine, or adjust the commands accordingly. Before starting, confirm the installed versions and that the Docker daemon is running.
Version and Environment Inventory
Before making any change, record the environment exactly as it is. This section covers what to check, why it matters, and the exact commands to run.
What to record
- Docker Engine version:
docker --version - Docker Compose version:
docker compose version(v2) ordocker-compose --version(v1) - Linux kernel and OS:
uname -aandcat /etc/os-release - Current user and Docker permissions:
id,docker info(check for permission errors) - Disk space for images and volumes:
df -h /var/lib/docker(or your Docker root directory) - Any relevant proxy or mirror settings for pulling images
Example:
$ docker --version
Docker version 24.0.7, build afdd53b
$ docker compose version
Docker Compose version v2.21.0
$ uname -a
Linux devbox 6.1.0-17-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.69-1 (2023-12-30) x86_64 GNU/Linux
If docker compose version returns "command not found", install the Docker Compose plugin (the standalone binary is deprecated for most distributions). For package-specific instructions, see the official Docker documentation for your OS.
Why version matters
The Compose file format is versioned, but modern Compose v2 ignores the version: top-level key and uses the latest schema. However, features like init, develop, or include require recent Compose versions. For example, docker compose include was added in v2.20. If you share a lab with teammates, pin the minimum supported Compose version in the project README.
Observing current state (read-only)
Use docker ps to see running containers. The format string below gives you names, status, and ports in a table:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
For a specific Compose project, docker compose ps shows containers belonging to that project (the directory name by default). To see all containers including stopped ones, add -a.
Example output:
NAMES STATUS PORTS
app-web-1 Up 2 hours 0.0.0.0:8080->80/tcp
app-db-1 Up 2 hours (healthy) 0.0.0.0:5432->5432/tcp
If a container is unhealthy, docker inspect --format '{{json .State.Health}}' app-db-1 shows the last few health check results.
Prerequisites for the examples in this article
- Docker Engine 20.10+ (older versions may work, but Compose v2 is recommended)
- Docker Compose v2.20+ (to use
includeif needed) - A user with permission to run Docker commands (either root or in the
dockergroup) - At least 2 GB of free disk space for images and volumes
- An internet connection to pull images (or a local registry mirror)
Smallest justified change
After recording the state, make only one scoped change at a time. For example, if you need to adjust a container's environment variable, edit the environment section of one service in docker-compose.yml. Do not simultaneously change volumes, networks, or image tags. After each change, recreate only the affected service with docker compose up -d --force-recreate <service> and verify.
Safe Configuration Path
A safe configuration path means every change is traceable, reversible, and testable. This section shows how to structure your Compose files, manage secrets, and ensure data persistence before and after changes.
Use a layered Compose setup
For local labs, a common pattern is a base docker-compose.yml plus an override file like docker-compose.override.yml for developer-specific tweaks. Compose automatically merges overrides, so you can keep the base file clean.
Base file docker-compose.yml:
services:
web:
image: nginx:1.25
ports:
- "8080:80"
volumes:
- app_data:/usr/share/nginx/html
environment:
- NGINX_HOST=localhost
- NGINX_PORT=80
networks:
- frontend
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
volumes:
- db_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
app_data:
db_data:
networks:
frontend:
backend:
Override file docker-compose.override.yml (optional):
services:
web:
ports:
- "8080:80"
environment:
- NGINX_HOST=dev.local
When you run docker compose up, both files are merged. The override can be disabled with docker compose -f docker-compose.yml up if you need the base config only.
Use environment variables and a .env file for non-secret settings
Compose automatically reads a .env file in the project directory for variable substitution in the Compose file. For example, to set the host port dynamically:
.env:
WEB_PORT=8080
POSTGRES_PASSWORD=mysecretpassword
docker-compose.yml:
services:
web:
ports:
- "${WEB_PORT}:80"
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
Never commit .env to version control if it contains real secrets. Instead, commit .env.example with placeholder values and document the required variables.
Protect secrets with Docker secrets or external tools
For local labs, you can use Docker secrets in Swarm mode, but for standalone Compose, better options are:
- Use a
.envfile that is ignored by git (add.envto.gitignore) - Use Docker's built-in secret support if you deploy to Swarm
- Use a secret manager like HashiCorp Vault or pass, and inject values at runtime
- Never hardcode passwords or API keys in the Compose file
Example .gitignore additions:
.env
docker-compose.override.yml
Verify configuration before applying
Before recreating containers, check the resolved configuration with docker compose config. This shows the merged Compose file with variables substituted, which helps catch typos and unintended overrides.
docker compose config
Example snippet of output:
services:
web:
image: nginx:1.25
ports:
- published: 8080
target: 80
volumes:
- app_data:/usr/share/nginx/html:rw
...
If you see a warning about an undefined variable, fix the .env file or set a default in the Compose file.
Data persistence best practices
Before changing container configuration, confirm where files are stored. A named volume such as app_data:/var/lib/app is managed by Docker and is usually easier to reuse across container rebuilds. A bind mount such as ./data:/var/lib/app maps a host directory directly and is useful for local development, but it can expose permission, portability, and backup problems if the same path does not exist on another machine.
In the example above, both app_data and db_data are named volumes. Test persistence with a restart test:
- Create some data:
docker compose exec db psql -U myuser -d myapp -c "CREATE TABLE test(id int); INSERT INTO test VALUES (1);"
- Stop and remove the containers:
docker compose down
- Start again:
docker compose up -d
- Verify the data is still there:
docker compose exec db psql -U myuser -d myapp -c "SELECT * FROM test;"
Expected output:
id
----
1
(1 row)
If the data disappears, the service was probably writing to the container filesystem instead of a volume or mount. Inspect with docker inspect <container> --format '{{json .Mounts}}' to see the actual mounts.
Verification and Diagnostics
Verification proves that a change achieved the desired state without side effects. Diagnostics help you understand what happened when it didn't. This section gives you concrete commands and expected outputs.
Verify container status and health
After docker compose up -d, wait a few seconds and check docker compose ps. Look for the STATUS column indicating Up and health status if defined.
docker compose ps
Example:
NAME COMMAND SERVICE STATUS PORTS
app-db-1 "docker-entrypoint.s…" db running (healthy) 0.0.0.0:5432->5432/tcp
app-web-1 "/docker-entrypoint.…" web running 0.0.0.0:8080->80/tcp
If healthcheck is configured, docker inspect --format '{{json .State.Health}}' app-db-1 gives JSON with "Status":"healthy" and the last log entries.
Read logs for failure signals
docker compose logs -f <service> follows logs. For a quick look at the last 100 lines:
docker compose logs --tail 100 web
When debugging startup failures, logs often show the exact error. For example, if the web service cannot connect to the database, you might see:
web_1 | Error: connect ECONNREFUSED 127.0.0.1:5432
Fix the network or environment and recreate the service.
Execute commands inside containers without changing the image
docker compose exec <service> sh opens a shell in the running container. This is useful for checking configuration files, testing connectivity, or running diagnostics:
docker compose exec web sh
Inside the container, you can run:
# Check nginx configuration
nginx -t
# Test listening ports
netstat -tulpn | grep 80
If the container is based on a minimal image without a shell, use docker compose run --rm <service> <command> instead, but be aware this creates a new container.
Inspect mounts, networks, and environment
docker inspect <container> provides a wealth of information. Use format templates to extract what you need:
# Mounts
docker inspect app-db-1 --format '{{json .Mounts}}'
# Networks
docker inspect app-db-1 --format '{{json .NetworkSettings.Networks}}'
# Environment variables
docker inspect app-db-1 --format '{{range .Config.Env}}{{println .}}{{end}}'
Example mounts output for the db container:
[{"Type":"volume","Name":"app_db_data","Source":"/var/lib/docker/volumes/app_db_data/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]
This confirms the database files are in a named volume, not ephemeral container storage.
Test application connectivity
For the web service, use curl from the host or from another container on the same network:
From host:
curl -I http://localhost:8080
Expected:
HTTP/1.1 200 OK
Server: nginx/1.25.3
From the db container to the web? Not applicable, but test db connectivity from web:
docker compose exec web sh -c "nc -zv db 5432"
Expected if nc is installed:
db (172.18.0.2:5432) open
If not, install netcat in the image or use ping and telnet alternatives.
Failure Modes and Recovery
Failures are inevitable. A good lab includes deliberate failure tests and documented recovery steps. This section covers common failure scenarios and how to recover safely.
Common failure modes
- Image pull failure - often due to network issues, registry authentication, or a typo in the image name.
- Symptom:
docker compose upfails withError response from daemon: pull access denied for ... - Recovery: Check
docker loginif using a private registry, verify the image name/tag, or use a mirror. For public images, retry later or check Docker Hub status.
- Port conflict - the host port is already in use.
- Symptom:
Error starting userland proxy: listen tcp4 0.0.0.0:8080: bind: address already in use - Recovery: Find the process using the port (
sudo lsof -i :8080orsudo ss -tulpn | grep 8080), stop it or change the Compose port mapping. Use127.0.0.1:8080:80to bind only to localhost if you don't need external access.
- Configuration error - invalid Compose file or unsupported option.
- Symptom:
docker compose configreturns errors, orupfails with a parsing error. - Recovery: Run
docker compose configto see the error line. Fix the YAML syntax or option, then re-run. Common issues: incorrect indentation, missingservices:key, or using v1 syntax.
- Data loss due to container recreation - when a container is recreated without a volume or mount.
- Symptom: after
docker compose up --force-recreate, data is missing. - Recovery: If the container had a volume, the data is safe. If it was using the container's writable layer, you may recover data from the old container if it still exists (
docker ps -ato find it, thendocker cpor commit it). To prevent, always use volumes for persistent data.
- Healthcheck failure - container is up but unhealthy, causing dependent services to fail.
- Symptom:
STATUSshowsrunning (unhealthy). - Recovery: Check
docker inspect --format '{{json .State.Health}}' <container>to see the failing health check. Then check logs and fix the underlying issue (e.g., database not ready, missing dependency). Adjust healthcheck parameters if too strict.
Recovery workflow
- Pinpoint the failing service:
docker compose psto see status. - Read logs:
docker compose logs <service>. - Inspect configuration:
docker compose configfor the service. - Make one fix (edit Compose file, change env var, update volume, etc.).
- Recreate only that service:
docker compose up -d --force-recreate <service>. - Verify health and application functionality.
- Document the incident and fix in your lab notes.
Example: Recovering from a bad environment variable
Suppose you accidentally set POSTGRES_PASSWORD to an empty string. The db container starts with no password, and the web app cannot authenticate.
Symptom:
web_1 | FATAL: password authentication failed for user "myuser"
Recovery:
- Check current env:
docker compose exec db env | grep POSTGRES_PASSWORD - Fix
.envfile to set a strong password. - Recreate db service:
docker compose up -d --force-recreate db - Verify:
docker compose exec db psql -U myuser -d myapp -c "SELECT 1;"should succeed.
Operations Checklist
Use this checklist before and after every change to your Docker Compose local lab.
Before any change
- [ ] Record current Docker and Compose versions.
- [ ] Run
docker compose psand note container statuses. - [ ] Back up any important data in volumes (e.g.,
docker run --rm -v app_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup.tar.gz -C /data .). - [ ] Review the Compose file with
docker compose configand confirm no unintended changes. - [ ] Ensure all secrets are in
.envor external manager, not hardcoded. - [ ] Identify the exact service(s) that will be affected.
- [ ] Prepare a rollback plan (e.g., revert the git commit or keep a backup Compose file).
After the change
- [ ] Run
docker compose up -d(or--force-recreateif needed). - [ ] Check
docker compose psfor expected status and health. - [ ] Run
docker compose logs <service>to catch immediate errors. - [ ] Verify application functionality (e.g.,
curlendpoint, run a DB query). - [ ] Test data persistence if volumes were involved (restart test).
- [ ] Update documentation or runbooks with the new state.
- [ ] Commit the final Compose file and
.env.exampleto version control.
Routine maintenance checklist
- [ ] Pull latest base images:
docker compose pull(then recreate if needed). - [ ] Clean up unused images and containers:
docker system prune -a(careful: this removes all unused images, not just dangling ones). - [ ] Check disk usage:
docker system df. - [ ] Review logs for recurring errors.
- [ ] Rotate any secrets used in the lab.
Conclusion
A Docker Compose local lab is only as safe as the discipline around it. This article provided a structured approach: inventory the environment, follow a safe configuration path, verify changes, understand failure modes, and use an operations checklist. Every recommendation is version-scoped, observable, and reversible where the technology permits.
Copying a command without checking prerequisites and expected output is not an operations procedure. As a next step, choose one low-risk verification from this article, record the current state, run the documented check, compare the result with the expected signal, and review dependencies such as Docker, Linux, and Nginx.
A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision. Build your lab with these principles, and it will serve as a trustworthy environment for development and testing.