E-NO
Docker Compose local lab 10 Min Read

Docker Compose Local Lab Setup with Practical Examples

calendar_today Published: 2026-08-21
update Last Updated: 2026-08-21
analytics SEO Efficiency: 100%
Technical guide illustration for Docker Compose Local Lab Setup with Practical Examples.

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) or docker-compose --version (v1)
  • Linux kernel and OS: uname -a and cat /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 include if needed)
  • A user with permission to run Docker commands (either root or in the docker group)
  • 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 .env file that is ignored by git (add .env to .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:

  1. Create some data:
   docker compose exec db psql -U myuser -d myapp -c "CREATE TABLE test(id int); INSERT INTO test VALUES (1);"
  1. Stop and remove the containers:
   docker compose down
  1. Start again:
   docker compose up -d
  1. 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

  1. Image pull failure - often due to network issues, registry authentication, or a typo in the image name.
  • Symptom: docker compose up fails with Error response from daemon: pull access denied for ...
  • Recovery: Check docker login if using a private registry, verify the image name/tag, or use a mirror. For public images, retry later or check Docker Hub status.
  1. 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 :8080 or sudo ss -tulpn | grep 8080), stop it or change the Compose port mapping. Use 127.0.0.1:8080:80 to bind only to localhost if you don't need external access.
  1. Configuration error - invalid Compose file or unsupported option.
  • Symptom: docker compose config returns errors, or up fails with a parsing error.
  • Recovery: Run docker compose config to see the error line. Fix the YAML syntax or option, then re-run. Common issues: incorrect indentation, missing services: key, or using v1 syntax.
  1. 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 -a to find it, then docker cp or commit it). To prevent, always use volumes for persistent data.
  1. Healthcheck failure - container is up but unhealthy, causing dependent services to fail.
  • Symptom: STATUS shows running (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

  1. Pinpoint the failing service: docker compose ps to see status.
  2. Read logs: docker compose logs <service>.
  3. Inspect configuration: docker compose config for the service.
  4. Make one fix (edit Compose file, change env var, update volume, etc.).
  5. Recreate only that service: docker compose up -d --force-recreate <service>.
  6. Verify health and application functionality.
  7. 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:

  1. Check current env: docker compose exec db env | grep POSTGRES_PASSWORD
  2. Fix .env file to set a strong password.
  3. Recreate db service: docker compose up -d --force-recreate db
  4. 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 ps and 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 config and confirm no unintended changes.
  • [ ] Ensure all secrets are in .env or 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-recreate if needed).
  • [ ] Check docker compose ps for expected status and health.
  • [ ] Run docker compose logs <service> to catch immediate errors.
  • [ ] Verify application functionality (e.g., curl endpoint, 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.example to 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.

Related Research

Article Quality Score

Reader usefulness 100%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL