ENV, -e, --env-file, ARG vs ENV, CMD vs ENTRYPOINT, exec vs shell form, PID 1 and SIGTERM.
2 · Storage
Why the writable layer disappears; volumes vs bind mounts vs tmpfs; the --mount syntax; volume lifecycle and backups.
3 · Networking, limits, health
User-defined bridges and DNS by container name, port publishing, --memory/--cpus, restart policies, HEALTHCHECK.
4 · Lab 03
Wire the whole Pinboard stack together by hand on pinboard-net with the pinboard-data volume — no Compose yet.
01
Configuration & the container process
One image, many environments — and one process that has to start and stop cleanly.
12-factor
Configuration comes from the environment
One pinboard-api:1.0 image everywhere; only the environment changes.
ENV in the Dockerfile = the default
-e KEY=value overrides it per container
--env-file for a whole set — no quoting, no shell expansion
Never bake secrets into the image
DB=postgres://pinboard:pinboard-secret@db:5432/pinboard
docker run -d --name api \
-e DATABASE_URL="$DB" -e APP_THEME=amber \
pinboard-api:1.0
# or the same set from a file
docker run -d --name api --env-file ./api.env pinboard-api:1.0
docker inspect api --format '{{json .Config.Env}}'
Don't confuse them
ARG vs ENV
ARG
ENV
Build time only
Build time and run time
Gone from the final image
Baked into the image config
--build-arg
-e / environment:
An ARG value is visible in docker history. Never pass a password that way — use --mount=type=secret.
Anonymous volumes (no src=) pile up invisibly — always name them
docker volume prune deletes everything unused: read the prompt
docker volume ls
docker volume inspect pinboard-data
docker volume rm pinboard-data # fails while in use
docker ps -a --filter volume=pinboard-data
docker system df -v # what is eating the disk
docker volume prune # unused volumes only
Operations
Backup with a throwaway container
# tar the volume out to the current host directory
docker run --rm \
--mount type=volume,src=pinboard-data,dst=/data \
--mount type=bind,src="$PWD",dst=/backup \
alpine tar czf /backup/pinboard-data.tgz -C /data .
# restore into a fresh volume
docker volume create pinboard-data-restored
docker run --rm \
--mount type=volume,src=pinboard-data-restored,dst=/data \
--mount type=bind,src="$PWD",dst=/backup \
alpine tar xzf /backup/pinboard-data.tgz -C /data
For a database, prefer the application-aware dump: docker exec db pg_dump -U pinboard pinboard > dump.sql.
Live Demo
Postgres with and without a volume
Lose the notes, then keep them — and bind-mount the Pinboard web folder to edit the page with the container running.
What you'll see · rm without a volume vs with pinboard-data; live HTML editFiles · src/db, src/pinboard-web/htmlDuration · 8 min
03
Networking, limits and health
How containers find each other, how you cap what they can consume, and how Docker knows they are alive.
Drivers
Network drivers you actually use
bridge (default)
The docker0 network every container joins if you say nothing. Works — but no DNS between containers.
User-defined bridge
You create it. Embedded DNS resolves container names, and only attached containers can reach each other.
host
No network namespace at all: the container uses the host's stack directly. Fast, zero isolation, Linux only.
none
Loopback only. For batch jobs that must not talk to anything.
Service discovery
Containers find each other by name
docker network create pinboard-net
docker run -d --name db --network pinboard-net \
--mount type=volume,src=pinboard-data,dst=/var/lib/postgresql/data \
-e POSTGRES_USER=pinboard -e POSTGRES_PASSWORD=pinboard-secret \
-e POSTGRES_DB=pinboard postgres:17-alpine
docker run -d --name api --network pinboard-net \
-e DATABASE_URL=postgres://pinboard:pinboard-secret@db:5432/pinboard \
-e APP_THEME=emerald pinboard-api:1.0
docker run -d --name web --network pinboard-net -p 8080:8080 \
-e API_URL=http://api:8080 pinboard-web:1.0
db, api and web are DNS names on pinboard-net. No IP addresses anywhere.
The door to the outside
Publishing ports
-p HOST:CONTAINER — only the host side is negotiable
Containers on the same network talk on the container port with no -p at all
EXPOSE in a Dockerfile is documentation, not a firewall rule
Bind to 127.0.0.1 to keep a port off the LAN
-p 8080:8080 # all interfaces
-p 127.0.0.1:5432:5432 # localhost only
-p 8080:8080/udp # protocol
-P # every EXPOSE → random port
docker port web
docker ps --format '{{.Names}}\t{{.Ports}}'
Guardrails
Resource limits and the OOM killer
By default a container may use all the host's CPU and RAM
--memory is a hard cap enforced by cgroups: exceed it and the kernel kills the process
An OOM-killed container exits with 137 and OOMKilled=true
--cpus is a quota, not a priority — the process is throttled, not killed
docker run -d --name api \
--memory=128m --memory-swap=128m \
--cpus=0.5 pinboard-api:1.0
docker stats --no-stream
docker inspect api \
--format '{{.State.OOMKilled}} {{.State.ExitCode}}'
Keep it running
Restart policies
Policy
Behaviour
Use it for
no
Default. Never restarted.
One-shot jobs, experiments
on-failure[:3]
Restart on a non-zero exit, up to N times
Batch work that may flake
always
Always restart, including after a daemon restart
Long-running services
unless-stopped
Like always, but respects a manual docker stop
What Pinboard uses
A restart policy reacts to the process exiting. An unhealthy-but-alive container is never restarted by Docker — that is an orchestrator's job.
Is it actually working?
HEALTHCHECK
States: starting → healthy / unhealthy
docker ps shows it in the STATUS column
Define it in the Dockerfile, or override with --health-cmd at run time
docker ps
# STATUS Up 40 seconds (healthy)
docker inspect web \
--format '{{.State.Health.Status}}'
Live Demo
Limits, restarts and the health column
Squeeze the API into 16 MB, watch it get killed, then bring the whole stack up on pinboard-net and read docker ps.
What you'll see · exit 137, docker stats, restart backoff, (healthy)Files · src/compose.yaml services, by handDuration · 7 min
Lab 03 · 30–40 min
Wire Pinboard together by hand
Create pinboard-net and pinboard-data, then run all three tiers so they find each other by name — and prove the notes survive the death of the database container.
You'll practise:network create · volume create · --mount · DATABASE_URL by name · API_URL · --memory · health & restart policies
Session 03
Key takeaways
Configure at run time
One image everywhere; -e / --env-file change behaviour. ARG is build time only.
Exec form or you lose signals
Your process must be PID 1 to receive SIGTERM and shut down gracefully.
State lives in volumes
The writable layer dies with the container. Named volumes for data, bind mounts for dev.
Next up
Session 04 — every flag from Lab 03, declared once in compose.yaml.
Questions?
Up next — Session 04: Docker Compose. Same stack, one file.