Chapter 37Lesson 02~225 minutes

Developer Workflows, Dev Containers, Inner Loop Optimization, Compose-Based Labs, and Local Parity: Guided Hands-On Workflow and Core Operations

Build a disposable Compose-based development workspace, run as a non-root user, compare bind and Watch inner loops, preserve caches deliberately, and build production separately.

Hands-onNon-rootCompose WatchCachesProduction boundary

Learning objectives

  • Create a disposable non-root Compose development environment with synthetic source and project-scoped state.
  • Compare bind-mounted source with a Compose Watch inner loop and observe which paths change.
  • Persist dependency/tool cache deliberately in a named volume instead of the source tree.
  • Run tests/tools inside the developer service, then build a production target separately.
  • Clean only the labeled project resources and retain an evidence directory for review.

1. Lab scope and safety

The lab uses only a local synthetic project. It publishes the demo service to loopback, uses no real credentials, and never mounts the Docker socket. Resource prefix: dca37-. The default path works with ordinary Docker Engine or Docker Desktop. Compose Watch is optional if your installed Compose is older than 2.22; the bind-mount path remains the faithful fallback.

2. Preflight and workspace

set -eu
LAB=dca37-lab
mkdir -p "$LAB/src" "$LAB/evidence"
date -u +%Y-%m-%dT%H:%M:%SZ | tee "$LAB/evidence/time-start.txt"
docker context show | tee "$LAB/evidence/context.txt"
docker version | tee "$LAB/evidence/docker-version.txt"
docker compose version | tee "$LAB/evidence/compose-version.txt"
git rev-parse HEAD 2>/dev/null | tee "$LAB/evidence/source-revision.txt" || printf 'synthetic-no-git
' | tee "$LAB/evidence/source-revision.txt"

3. Create synthetic application and test

# dca37-lab/src/app.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import os

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = f"dev-loop={os.getenv('DEV_LOOP','unknown')}\n".encode()
        self.send_response(200)
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
# dca37-lab/src/test_app.py
import pathlib
text = pathlib.Path("/workspace/src/app.py").read_text()
assert "HTTPServer" in text
print("source-test=pass")

4. Development Dockerfile: non-root and writable tool paths

# dca37-lab/Dockerfile.dev
# syntax=docker/dockerfile:1
FROM python:3.13-alpine
ARG DEV_UID=10001
ARG DEV_GID=10001
RUN addgroup -g ${DEV_GID} dev && adduser -D -u ${DEV_UID} -G dev dev     && mkdir -p /workspace/src /home/dev/.cache     && chown -R dev:dev /workspace /home/dev
WORKDIR /workspace
USER dev
CMD ["python", "/workspace/src/app.py"]

The image does not install the host’s real username. Numeric identity is the boundary that matters. The default UID/GID values are lab-owned; on native Linux you may set them to your own IDs for a bind-mount experiment.

5. Bind-mount development path

# dca37-lab/compose.bind.yaml
services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
      args:
        DEV_UID: ${DEV_UID:-10001}
        DEV_GID: ${DEV_GID:-10001}
    user: "${DEV_UID:-10001}:${DEV_GID:-10001}"
    working_dir: /workspace
    command: ["python", "/workspace/src/app.py"]
    environment:
      DEV_LOOP: bind
    ports:
      - "127.0.0.1:18037:8000"
    volumes:
      - ./src:/workspace/src
      - dca37-cache:/home/dev/.cache
    labels:
      academy.lab: dca37
volumes:
  dca37-cache:
    name: dca37-cache
    labels:
      academy.lab: dca37

6. Native-Linux ownership alignment

# Native Linux only; skip on Docker Desktop if these host IDs do not model your VM path.
export DEV_UID=$(id -u)
export DEV_GID=$(id -g)
docker compose -f dca37-lab/compose.bind.yaml config | tee dca37-lab/evidence/compose-bind.rendered.yaml
docker compose -p dca37-bind -f dca37-lab/compose.bind.yaml up -d --build

On native Linux, files created through the bind mount will use the configured numeric identity. On macOS/Windows Docker Desktop, the host↔VM sharing layer changes ownership behavior; record observations rather than forcing permissions with chmod 777.

7. Inspect identity, mounts, cache, and port

docker compose -p dca37-bind -f dca37-lab/compose.bind.yaml exec dev id | tee dca37-lab/evidence/dev-id.txt
docker compose -p dca37-bind -f dca37-lab/compose.bind.yaml exec dev python /workspace/src/test_app.py | tee dca37-lab/evidence/test-bind.txt
docker inspect dca37-bind-dev-1 --format '{{json .Mounts}}' | tee dca37-lab/evidence/mounts-bind.json
docker port dca37-bind-dev-1 | tee dca37-lab/evidence/ports-bind.txt
docker volume inspect dca37-cache | tee dca37-lab/evidence/cache-volume.json

8. Exercise the bind inner loop

printf '
# edit-marker=bind-loop
' >> dca37-lab/src/app.py
docker compose -p dca37-bind -f dca37-lab/compose.bind.yaml exec dev tail -n 2 /workspace/src/app.py | tee dca37-lab/evidence/bind-edit-visible.txt

The edit appears because the container is looking directly through the bind path. That is fast on native Linux but can be more expensive across Desktop’s host/VM file-sharing boundary for large repositories.

9. Compose Watch alternative

# dca37-lab/compose.watch.yaml
services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: ["python", "/workspace/src/app.py"]
    environment:
      DEV_LOOP: watch
    ports:
      - "127.0.0.1:18038:8000"
    volumes:
      - dca37-watch-cache:/home/dev/.cache
    develop:
      watch:
        - action: sync+restart
          path: ./src
          target: /workspace/src
          initial_sync: true
        - action: rebuild
          path: Dockerfile.dev
    labels:
      academy.lab: dca37
volumes:
  dca37-watch-cache:
    name: dca37-watch-cache
    labels:
      academy.lab: dca37

Start with docker compose -p dca37-watch -f dca37-lab/compose.watch.yaml up --watch in an interactive terminal. sync+restart updates the files then restarts the service; the Dockerfile rule rebuilds because a toolchain change belongs in the image, not a file copy.

10. Production Dockerfile is a separate target

# dca37-lab/Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-alpine AS runtime
RUN addgroup -g 10001 app && adduser -D -u 10001 -G app app
WORKDIR /app
COPY --chown=app:app src/app.py /app/app.py
USER app
EXPOSE 8000
CMD ["python", "/app/app.py"]
docker build -f dca37-lab/Dockerfile -t dca37-prod:checkpoint dca37-lab
docker image inspect dca37-prod:checkpoint --format 'id={{.Id}} user={{.Config.User}} workdir={{.Config.WorkingDir}}' | tee dca37-lab/evidence/prod-image.txt

11. Compare development and production explicitly

Property Development Production
Source bind or Watch-updated copied at image build
Cache named development volume none unless application requires runtime data
Tooling developer shell/tools may exist only runtime requirement
Ports loopback convenience publish deployment ingress decided elsewhere
Mutability workspace intentionally changes image content immutable
User development UID strategy fixed app UID 10001 in example

12. Exact cleanup

docker compose -p dca37-bind -f dca37-lab/compose.bind.yaml down
docker compose -p dca37-watch -f dca37-lab/compose.watch.yaml down 2>/dev/null || true
docker volume rm dca37-cache dca37-watch-cache 2>/dev/null || true
docker image rm dca37-prod:checkpoint 2>/dev/null || true
printf 'Keep dca37-lab/evidence until reviewed.
'

The cleanup names only lab-owned projects, volumes, and image. It does not invoke any global prune.

13. Challenge: choose the layer

A developer edits src/app.py, but the running service does not change. First identify the inner-loop mechanism. With a bind mount, inspect the container mount source/target and file visibility. With Watch, inspect the Watch rule, ignored paths, target writability, and Watch process output. Rebuilding the entire daemon or deleting caches is not a first response.

Knowledge check

Why does the lab use a named volume for /home/dev/.cache?

Why is the production image built separately?

What is the safe response if UID mapping causes a write failure?

When should a Watch rule use rebuild?

Does docker compose down delete the named cache volumes by default?

Next lesson

Next: Developer Workflows, Dev Containers, Inner Loop Optimization, Compose-Based Labs, and Local Parity: Configuration, Design Choices, and Tradeoffs

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

Official references and version notes

Lab baseline: 2026-09-22. The mandatory path is free/local/disposable. Compose Watch requires Compose 2.22+; if unavailable, use the bind path and preserve the same evidence contract.

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.