Docker Images and Containers — Why Unpinned Tags Break
Unpinned FROM python:3.
- Image: read-only layers + metadata (CMD, ENV, EXPOSE)
- Container: image + writable layer + process
- Registry: stores and distributes images (Docker Hub, ECR)
- Dockerfile: recipe that produces an image
A Docker image is like a recipe card — it describes exactly what ingredients and steps are needed to produce a dish. A container is the actual dish made from that recipe. You can make many dishes (containers) from the same recipe (image), and each dish can be slightly customized (environment variables, mounted volumes), but the base recipe never changes.
Building an Image with Dockerfile
A Dockerfile is a recipe for building an image. Each instruction creates a new layer — a filesystem diff on top of the previous layer. Docker caches layers and only rebuilds from the first changed instruction downward. Understanding this is the single most important optimization for build speed.
The layer caching principle: if a layer has not changed and all preceding layers are cached, Docker reuses the cached layer instantly. Dependencies change rarely. Code changes frequently. Put rare changes first.
The .dockerignore file controls what gets sent to the Docker Daemon as build context. Without it, your entire project directory — including .git (often 100MB+), node_modules, __pycache__, and .env files with secrets — is sent on every build. This slows builds and risks secret exposure.
- Docker caches layers top-to-bottom. A changed instruction invalidates all subsequent layers.
- Dependencies change rarely. Code changes frequently. Put rare changes first.
- COPY requirements.txt before COPY . . ensures pip install is cached on code-only changes.
- Each RUN creates a layer. Combining with && reduces layer count and image size.
Running Containers
A container is a running instance of an image with an additional thin writable layer. Multiple containers can run from the same image — each with its own writable layer, environment variables, and port mappings. The image itself never changes.
The container lifecycle: create (allocate resources), start (run the CMD process), stop (send SIGTERM, wait, then SIGKILL), remove (delete the writable layer). The --rm flag automates removal on stop.
Port mapping confusion: -p HOST:CONTAINER maps the host port to the container port. Your application inside the container must bind to 0.0.0.0 (all interfaces), not 127.0.0.1 (localhost). Binding to localhost inside the container means the application only accepts connections from within the container — the host port mapping becomes useless.
The difference between stop and kill: docker stop sends SIGTERM (graceful shutdown, 10-second default timeout), then SIGKILL if the process does not exit. docker kill sends SIGKILL immediately. Always use stop in production — it gives your application time to flush logs, close database connections, and complete in-flight requests.
- 127.0.0.1 inside the container is the container's own loopback, not the host's.
- Port mapping (-p 8000:8000) forwards traffic from the host to the container's network interface.
- If the app binds to 127.0.0.1, it only accepts connections from inside the container.
- Binding to 0.0.0.0 accepts connections from all interfaces, including the mapped port.
Volumes and Networking
Containers are ephemeral — when a container stops, its writable layer is discarded. For state that must survive container restarts (databases, file uploads, logs), you need volumes.
Named volumes: Managed by Docker. The storage location is controlled by the Docker Daemon (typically /var/lib/docker/volumes/). Survives docker compose down. Destroyed only by docker compose down -v or docker volume rm. Best for databases.
Bind mounts: Mount a host directory into the container. Changes on either side are reflected immediately. Great for development (hot reload). Not recommended for production — ties the container to a specific host path and breaks portability.
Networking: Containers on the same Docker network can reach each other by container name. Docker's embedded DNS server (127.0.0.11) resolves container names to internal IPs. The host-mapped port (left side of -p) is for external access. Container-to-container communication uses the container port directly — never the host port.
Volume lifecycle gotcha: Named volumes persist data even after the container is removed. This is a feature for databases but a trap for test environments — stale data from previous test runs can cause non-deterministic test failures. Use docker compose down -v in CI to ensure clean state.
- Named volumes are portable — Docker manages the storage location, not a host path.
- Bind mounts couple the container to a specific host — breaks multi-machine deployments.
- Named volumes survive docker compose down. Bind mounts depend on the host directory existing.
- Named volumes use Docker's optimized storage driver (overlay2). Bind mounts go through the host filesystem.
Production Outage from Unpinned Base Image — python:3.12-slim Changed OS Underneath
- Unpinned tags are time bombs. python:3.12-slim is not a fixed target — it moves.
- Pin both the version AND the OS codename: python:3.12.3-slim-bookworm.
- CI runners without warm caches pull the latest image on every build, exposing you to silent upstream changes.
- A 20-character change in a FROM line prevents hours of incident response.
- Image digest pinning (FROM python:3.12.3-slim-bookworm@sha256:abc123...) is the strongest guarantee.
Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
That's Docker. Mark it forged?
3 min read · try the examples if you haven't