Chapter 15Lesson 02~120 minutes

Docker Compose Foundations: Services, Images, Builds, Networks, Volumes, Environment, and Project Lifecycle: Guided Hands-On Workflow and Core Operations

Build and operate a disposable three-service Compose project while observing normalized configuration, project resources, service DNS, named-volume persistence, logs, rebuilds, and bounded exec diagnostics.

compose upconfigps / logsexecpersistent data

Learning objectives

  • Create a disposable Compose project only after validating and recording its normalized configuration.
  • Observe project-scoped containers, default network, named volume, canonical labels, image identity, and published-port state.
  • Use up, ps, logs, exec, build, stop, start, and down while explaining exactly which state each changes.
  • Verify service-name connectivity independently from host-port reachability and verify named-volume persistence across container replacement.
  • Clean only the disposable project resources and preserve data unless deletion is an explicit lab objective.
Lab boundary. This exercise is local and disposable. It uses no cloud service, no registry credentials, no Docker socket mount, no privileged container, and no real secret. The host port binds to loopback only. Run it on an authorized Docker host.

1. Preflight and evidence directory

Use a fresh directory. The command blocks use POSIX shell syntax; Windows users can run them in Git Bash/WSL, or create the same files with PowerShell/editor. First capture the execution context so a later failure cannot be misattributed to the Compose model.

mkdir -p da-compose15/web da-compose15/evidence
cd da-compose15

docker version > evidence/docker-version.txt
docker info > evidence/docker-info.txt
docker context show > evidence/docker-context.txt
docker compose version > evidence/compose-version.txt
docker buildx version > evidence/buildx-version.txt

Prediction 1: before up, there should be no resources carrying project label da-compose15. Verify rather than assume:

docker ps -a --filter label=com.docker.compose.project=da-compose15
docker network ls --filter label=com.docker.compose.project=da-compose15
docker volume ls --filter label=com.docker.compose.project=da-compose15

2. Declare a small application: one built web service, one writer, one probe

The web service is built locally so the lab exercises Compose build integration. A writer stores a synthetic value in a named volume. The same volume is mounted read-only into the web service so the data can be retrieved through HTTP. A probe service accesses web by service name over the default Compose network.

Create web/index.html:

<!doctype html>
<html><body><h1>DevOps Academy Compose 15</h1><p>Static page from the built web image.</p></body></html>

Create web/Dockerfile. The human-readable base tag is intentionally versioned; record the resolved base digest/build evidence on your host because tags remain mutable.

# syntax=docker/dockerfile:1
FROM nginx:1.29.1-alpine
COPY index.html /usr/share/nginx/html/index.html
USER 101
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
Compatibility note. Some Nginx images/configurations bind port 80 as root and drop privileges later. This lab instead demonstrates a least-privilege intent but image defaults can differ. If your exact image cannot bind the configured port as UID 101, preserve the failure evidence and use the image's documented unprivileged variant or adjust the lab to a non-privileged port; do not add broad privilege.

For maximum portability in the mandatory path, use this alternative web/Dockerfile, which needs no privileged port:

# syntax=docker/dockerfile:1
FROM busybox:1.36.1
COPY index.html /site/index.html
USER 65534:65534
WORKDIR /site
EXPOSE 8080
CMD ["httpd", "-f", "-p", "8080", "-h", "/site"]

Create .env with non-secret configuration only:

WEB_PORT=18080
LAB_MESSAGE=compose-volume-survives-container-replacement

Create compose.yaml:

name: da-compose15
services:
  web:
    build:
      context: ./web
    image: da-compose15-web:lab
    ports:
      - "127.0.0.1:${WEB_PORT:?set WEB_PORT}:8080"
    volumes:
      - appdata:/site/state:ro
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 5s
      timeout: 2s
      retries: 5
    labels:
      academy.lesson: "docker-compose-15"

  writer:
    image: busybox:1.36.1
    environment:
      LAB_MESSAGE: "${LAB_MESSAGE:?set LAB_MESSAGE}"
    command: ["sh", "-c", "printf '%s\n' "$$LAB_MESSAGE" > /state/message.txt; exec sleep 3600"]
    volumes:
      - appdata:/state

  probe:
    image: busybox:1.36.1
    command: ["sh", "-c", "while :; do wget -q -O - http://web:8080/ >/dev/null && echo web-ok || echo web-not-ready; sleep 10; done"]

volumes:
  appdata: {}

$$LAB_MESSAGE deliberately escapes Compose interpolation so the variable is expanded by the shell inside the writer container. This distinction is observable with docker compose config.

3. Normalize before mutating: validate inputs and save the model

docker compose config --quiet
docker compose config --environment | tee evidence/interpolation-environment.txt
docker compose config | tee evidence/compose-normalized.yaml
docker compose config --services | tee evidence/services.txt
docker compose config --networks | tee evidence/networks.txt
docker compose config --volumes | tee evidence/volumes.txt

If the required variables are missing, the :? syntax fails early instead of silently creating an invalid/ambiguous runtime configuration. Do not continue until the normalized output matches your intent.

4. Create and start the project with up

Prediction 2: Compose will create three service containers, a project default network, and one named volume; it will build the web image and publish only its loopback host port.

docker compose up -d --build

docker compose ps -a | tee evidence/compose-ps.txt
docker compose images | tee evidence/compose-images.txt
docker network ls --filter label=com.docker.compose.project=da-compose15 | tee evidence/networks-runtime.txt
docker volume ls --filter label=com.docker.compose.project=da-compose15 | tee evidence/volumes-runtime.txt

-d changes terminal attachment, not the service definition. --build asks Compose to build services with build definitions before/reconciliation during start. Capture the build output if you need exact BuildKit evidence.

5. Inspect resource identity, labels, mounts, ports, and image state

docker compose ps --format json > evidence/compose-ps.json
docker compose images > evidence/images.txt

docker inspect "$(docker compose ps -q web)" > evidence/web-inspect.json
docker inspect "$(docker compose ps -q writer)" > evidence/writer-inspect.json

docker image inspect da-compose15-web:lab --format '{{.Id}} {{json .RepoDigests}}' | tee evidence/web-image-identity.txt

Inspect should show a Compose project/service label set, network attachment, read-only volume mount on web, writable mount on writer, and a host port bound to loopback. These are concrete Engine states produced from the Compose model.

6. Verify service-to-service DNS separately from host publication

docker compose exec probe sh -c 'nslookup web || true; wget -q -O - http://web:8080/'
docker compose exec web sh -c 'cat /site/state/message.txt'

# Host-side path (if curl is installed on the host):
curl -fsS http://127.0.0.1:18080/

The first request proves network-scoped service discovery and application reachability from another service. The host request proves the published path. A success on one path does not prove the other.

7. Use logs and bounded exec diagnostics without mutating deployment intent

docker compose logs --no-color --tail=50 web writer probe | tee evidence/logs-tail.txt
docker compose exec web sh -c 'id; pwd; ls -l /site /site/state; cat /site/state/message.txt'
docker compose exec writer sh -c 'id; cat /state/message.txt' 

exec is diagnostic runtime activity. It does not update compose.yaml or the image. If you discover an intended change, encode it in source/build/configuration and reconcile the service instead of treating an interactive edit as deployment.

8. Rebuild one service and prove persistent data is independent

Change one line in web/index.html, then rebuild and reconcile only web:

docker compose build web
docker compose up -d web

docker compose ps
docker compose exec web cat /site/state/message.txt

The web container may be replaced because its image/config changed, but the named volume remains a separate storage object. The state file should still exist.

9. Stop/start changes process state without deleting project resources

docker compose stop probe
docker compose ps -a
docker compose start probe
docker compose ps

stop stops service containers. start starts existing stopped containers. Neither operation rebuilds images or intentionally removes networks/volumes.

10. Challenge: locate the failing layer before copying a command

Suppose curl http://127.0.0.1:18080/ fails, but docker compose exec probe wget -q -O - http://web:8080/ succeeds. Which layer is most likely healthy, and which path needs investigation?

Reason before acting: service DNS, internal network attachment, and the web process are probably healthy enough for the internal request. Inspect the host-published port/binding, selected context, host firewall/listener path, and Compose port configuration. Rebuilding the image or deleting the named volume is unrelated.

11. Tear down with data-preservation intent explicit

docker compose down

# Containers and project network should be gone; named volume should remain:
docker ps -a --filter label=com.docker.compose.project=da-compose15
docker network ls --filter label=com.docker.compose.project=da-compose15
docker volume ls --filter label=com.docker.compose.project=da-compose15

Do not add the volume-deletion option casually. If the lab objective is eventually to remove the exact disposable volume, first identify it by Compose labels, confirm no other workload uses it, and remove that exact volume explicitly. Avoid broad prune operations.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Now that the mechanics are observable, choose deliberately between images/builds, networks, storage forms, environment sources, project naming, and foreground/detached workflows.

Knowledge check

Why run docker compose config before up?

What state does docker compose exec change?

If service-to-service HTTP works but the host-published port fails, should you rebuild the image first?

Why does the volume survive a web container replacement?

What does down remove by default?

Official references and version notes

Version baseline checked 2026-09-21.

Upstream Compose is v5.5.1. The course still treats docker compose version, docker version, docker info, and the active context as execution evidence. The top-level Compose version: field is obsolete/informative; the current Compose implementation validates against the current schema.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.