Containerise with Docker
Activated Cloud✓ Officialactivated/containerize-with-docker
Free · MIT
About
Writes and debugs Dockerfiles and Compose stacks that build fast, run as non-root, stay small and behave in production: multi-stage builds, pinned base images, cache-friendly layer order, a strict .dockerignore, no secrets in layers, exec-form commands, health checks and readiness-aware Compose dependencies. Includes a method for containers that exit, cannot be reached, lack permissions or fill the disk. Use when asked to containerise an app, fix a broken image or compose file, or shrink an image. Not for rolling releases out to servers (use deploy-and-rollback).
Documentation
Containerise with Docker
A good image is small, reproducible, runs as an unprivileged user, holds no secrets, starts fast and shuts down cleanly. A good Compose file starts services in the right order and keeps data in named volumes. You build it, run it, prove it answers, and inspect what is inside before calling it done.
When to use
- "Dockerise this app", "write a Dockerfile", "add a compose file for local dev".
- "The container keeps exiting", "I can't reach the app in the container", "the image is 2 GB", "builds take forever".
- Reviewing someone's Dockerfile or Compose file.
What you need
- The app's runtime and version, how it is built, how it starts, which port it listens on, which config it reads (environment variables), and what state it keeps (uploads, databases).
- Docker on your computer (
docker version,docker compose version). If it is missing or you lack permission to use it, say so and ask the owner rather than usingsudoon shared machines. - Registry access only if you must push images: through the owner's connected app or a CI pipeline. Pushing to a shared registry needs the owner's go-ahead.
Method
Learn the app before writing anything. Read its manifest, start script and config loading. Run it once outside Docker if you can, so you know what "working" looks like.
Write the Dockerfile from the matching template in
references/dockerfile-templates.md(Python, Node, Go, Rust), then adapt. The rules:- Base image: an official or verified image, slim or distroless where the app allows; pin a specific version tag (
python:3.12-slim,node:22-bookworm-slim), and for production pin the digest too (@sha256:...). Neverlatest. - Multi-stage: build tools and dev dependencies in a builder stage; copy only the runtime artefacts into the final stage.
- Layer order for caching: copy the dependency manifests and lockfile first, install dependencies, then copy the source. A source change then reuses the dependency layer.
- Package installs:
apt-get update && apt-get install -y --no-install-recommends <pkgs> && rm -rf /var/lib/apt/lists/*in oneRUN; pin versions where reproducibility matters. COPY, notADD, unless you needADD's remote-URL-with-checksum or archive extraction on purpose.- Non-root: create a user and switch with
USERbeforeCMD. Make only the directories the app writes to owned by that user. - Exec form for
CMDandENTRYPOINT(CMD ["gunicorn", "app:app"]) so the process is PID 1 and receives stop signals; if the app does not handle signals or reap children, run with--initor addtini. - Listen on 0.0.0.0 inside the container, not 127.0.0.1.
- No secrets in the image: not in
ENV,ARGor copied files. Pass runtime secrets as environment variables or mounted files; for build-time secrets use BuildKit secret mounts (RUN --mount=type=secret,id=npmrc ...). - Health check:
HEALTHCHECKagainst a cheap endpoint, using a tool that exists in the image (slim images often lackcurl).
- Base image: an official or verified image, slim or distroless where the app allows; pin a specific version tag (
Write a strict
.dockerignore:.git,.env*,node_modules,.venv,__pycache__,dist,build,target, coverage output, local databases, editor folders, and any secrets. Without it, the build context leaks secrets into layers and invalidates the cache on every change.Build and inspect.
docker build -t app:dev . docker image ls app:dev # size docker history app:dev # which layers are big docker run --rm app:dev id # must not be uid 0 docker run --rm --entrypoint sh app:dev -c 'ls -la /app; env | sort' # nothing that should not be thereBase images without a shell (distroless) need
--entrypointpointed at the app binary instead.Run it and prove it works.
docker run -d --name app-test -p 8080:8080 --env-file .env.example app:dev sleep 2; docker ps --filter name=app-test; docker logs app-test curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8080/health docker stop app-test && docker rm app-testAlso check it stops promptly (
time docker stop app-testshould take well under the 10-second default; longer means signals are not reaching the app).Compose for multi-service setups. Use the template in
references/dockerfile-templates.md:- one service per process; services reach each other by service name;
healthcheckon dependencies anddepends_on: { db: { condition: service_healthy } }on dependents;- named volumes for data, bind mounts only for source code in development;
env_filefor configuration, with secrets kept out of the compose file;restart: unless-stoppedfor long-running services. Validate withdocker compose config -q, start withdocker compose up -d --build, checkdocker compose psshows healthy, and readdocker compose logs --tail 50.
Shrink if needed. Check
docker historyfor the big layers; usual wins are multi-stage builds, slimmer bases, removing build tools and caches in the same layer that created them, and excluding test data via.dockerignore. Measure before and after.Scan if the project does. An open-source scanner such as Trivy (
trivy image app:dev) lists known vulnerable packages; fix by updating the base image or packages. Report findings rather than ignoring them.Debug failures with
references/dockerfile-templates.md(troubleshooting table): exits immediately, cannot connect, permission denied on volumes, no space left, cache never hits, works on one machine but not another (CPU architecture,--platform linux/amd64).Ask before destroying.
docker compose down -v,docker volume rm,docker system prune -a --volumesand removing other people's containers or images delete data. Show what would be removed (docker system df -v) and get the owner's go-ahead.
Output
The Dockerfile, .dockerignore and compose file (if any), plus a note with: image size, the user it runs as, the build and run commands, the health check result, how long docker stop takes, and anything unresolved (scan findings, assumptions about config).
Checks before you finish
- The image builds from a clean checkout, and
docker runserves a healthy response. - The process runs as a non-root user and stops cleanly on
docker stop. - No secrets in the image history, environment or files;
.dockerignoreexcludes.envand.git. - Base images are pinned to a version (and digest for production).
- Compose services become healthy in order, and data lives in named volumes.
Pitfalls
latesttags. Builds change under you without a code change.- Copying the whole context before installing dependencies. Every source edit reinstalls everything.
- Secrets in
ENVorARG. They stay in the image history even if a later layer deletes them. - Shell-form
CMD. The shell becomes PID 1, swallows SIGTERM, and every stop takes the full timeout before a kill. - Binding to localhost inside the container. The port mapping cannot reach it.
depends_onwithout a health condition. Compose only waits for the container to start, not for the database to accept connections.- Running as root "because permissions". Fix the ownership of the specific directories instead.
Versions
Listed from the source repository.
Reviews
No reviews yet. Be the first.
