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.
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, anddownwhile 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.
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;"]
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.
Knowledge check
Why run docker compose config before
up?
It exposes the resolved model before Engine resources are changed, catching interpolation and model errors at the cheapest layer.
What state does
docker compose exec change?
It starts a bounded additional process inside an already-running service container. It does not alter the image or Compose source model.
If service-to-service HTTP works but the host-published port fails, should you rebuild the image first?
Not by default. Internal reachability already supports the image/process/network path; inspect port publication, host binding, context, and host network/firewall evidence.
Why does the volume survive a web container replacement?
The named volume is an independent Engine storage object attached to containers, not part of a container writable layer.
What does down remove by default?
Project service containers and project networks. Named volumes are preserved unless explicit volume deletion is requested; external resources are not removed.
Official references and version notes
- Docker Docs — Compose application model: projects, services, networks, volumes, and lifecycle.
- Docker Docs — Compose Specification reference: current declarative model and attributes.
-
Docker Docs —
docker compose config: canonical rendering, interpolation environment, service/network/volume views, hashes and digest resolution. - Docker Docs — project naming: precedence and isolation.
- Docker Docs — interpolation and container environment precedence.
- Docker Docs — Compose networking: default network and service-name discovery.
-
Docker Docs —
docker compose down: default teardown and explicit volume deletion semantics. - Docker Compose v5.5.1 release (2026-09-03). Labs record the actually installed version and do not assume every host matches upstream.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.