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.
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?
It separates performance state from source and avoids unnecessary host-file-sharing overhead while retaining explicit project lifecycle.
Why is the production image built separately?
A long-lived dev container contains mutable workspace/tool state; production must come from reviewed build inputs and a controlled Dockerfile target.
What is the safe response if UID mapping causes a write failure?
Inspect numeric UID/GID and mount ownership, then align intended ownership or use a dedicated writable path; do not use broad world-write permissions.
When should a Watch rule use rebuild?
When the changed file alters image/tool/dependency state that cannot be correctly applied by copying source into the running container.
Does docker compose down delete the named cache
volumes by default?
No. The lab removes its exact named volumes explicitly after the Compose projects are down.
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.
- Docker Docs — Use Compose Watch — current Watch workflow, sync/rebuild behavior, ownership guidance, and command forms.
-
Docker Docs — Compose Develop Specification
—
develop.watch, actions, paths, targets, ignore/include, and version gates. - Docker Docs — Docker Desktop settings — host/VM file sharing and resource behavior.
- Docker Docs — Synchronized file shares — optional Desktop acceleration for large repositories and its constraints.
- Docker Docs — Sharing local files — bind-mount semantics and host-side effects.
- Development Containers Specification — open development-container specification and reference ecosystem.
-
Dev Container Spec — Overview
—
devcontainer.jsonas development metadata layered onto container technologies. - Dev Container Spec — Dockerfile and Compose guide — using Dockerfile/Compose as the underlying environment definition.
-
Dev Container Spec — Supporting tools and services
— support boundaries for properties such as
remoteUserandforwardPorts. - Dev Container Features index — current Feature registry and versioned references.
- Dev Container Spec — Feature updates and lockfiles — resolved Feature identities and lockfile maintenance.
- Docker Docs — Multi-stage builds — keep development tooling out of the production target.
- Docker Engine 29 release notes — current Engine-era context used by this course.
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.