Docker Images and Containers
- Images are immutable layers; containers add a thin writable layer on top — changes do not affect the image.
- Layer caching: copy requirements.txt and install before copying source code for faster builds.
- EXPOSE documents a port but does not publish it — use -p HOST:CONTAINER to publish.
- 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
Container crashed or restarting in a loop.
docker logs --tail 50 <container>docker inspect <container> --format='{{.State.ExitCode}} {{.State.OOMKilled}}'Container running but not responding to requests.
docker port <container>docker exec <container> ps auxdocker build fails with 'no space left on device'.
docker system dfdocker system prune -a --volumesImage build is very slow, reinstalls dependencies every time.
docker history <image>docker build --progress=plain -t test . 2>&1 | grep CACHEDContainer works locally but fails in CI or on another machine.
docker inspect --format='{{.Config.Image}}' <container>docker inspect --format='{{.Architecture}}' <image>Production Incident
Production Debug GuideFrom failed container to root cause — systematic debugging paths.
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.
# Best practices Dockerfile for a Python FastAPI app FROM python:3.12-slim # slim variant: much smaller than full python image WORKDIR /app # Copy requirements FIRST for layer caching # If code changes but requirements don't, this layer is cached COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Now copy application code COPY . . # Run as non-root for security RUN useradd -m appuser USER appuser # Document the port (does not actually publish it) EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
- 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.
# Run a container docker run -p 8000:8000 myapp:1.0 # Flags: # -p HOST_PORT:CONTAINER_PORT — publish port # -d — detached (background) # -e DATABASE_URL=postgres://.. — environment variable # -v /host/path:/container/path — bind mount volume # --name my-container — give it a name # --rm — remove when stopped docker run -d \ --name api-server \ -p 8000:8000 \ -e DATABASE_URL=postgres://localhost/mydb \ --rm \ myapp:1.0 # Container lifecycle docker ps # running containers docker ps -a # all containers (including stopped) docker stop api-server # graceful stop (SIGTERM) docker kill api-server # force stop (SIGKILL) docker rm api-server # remove stopped container docker logs api-server # view logs docker logs -f api-server # follow logs docker exec -it api-server bash # shell into running container
- 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: data persists after container is removed docker volume create postgres-data docker run -d \ -v postgres-data:/var/lib/postgresql/data \ -e POSTGRES_PASSWORD=secret \ postgres:16 # Container networking: containers on same network can reach each other by name docker network create myapp-network docker run -d --name db --network myapp-network postgres:16 docker run -d \ --name api \ --network myapp-network \ -e DATABASE_URL=postgres://db/myapp \ myapp:1.0 # 'api' container reaches 'db' by hostname 'db' # Image management docker image ls # list images docker image rm myapp:1.0 docker system prune -a # remove all unused images, containers, networks
- 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.
| Characteristic | Image | Container | Volume |
|---|---|---|---|
| What it is | Immutable template (read-only layers) | Running instance of an image | Persistent storage outside container lifecycle |
| Mutability | Immutable — never changes after build | Writable layer on top of image | Fully writable, persists independently |
| Created by | docker build (from Dockerfile) | docker run (from image) | docker volume create or Compose |
| Survives removal | Yes (until explicitly deleted) | No (writable layer discarded) | Yes (until explicitly deleted) |
| Shared between | Multiple containers | Single container instance | Multiple containers |
| Storage location | Docker Daemon storage (/var/lib/docker) | Docker Daemon storage (writable layer) | Docker-managed or host path (bind mount) |
| Size impact | Determined by layers and base image | Image size + writable layer delta | Independent of image/container size |
| Use case | Package application + dependencies | Run the application | Persist database, uploads, logs |
🎯 Key Takeaways
- Images are immutable layers; containers add a thin writable layer on top — changes do not affect the image.
- Layer caching: copy requirements.txt and install before copying source code for faster builds.
- EXPOSE documents a port but does not publish it — use -p HOST:CONTAINER to publish.
- Named volumes persist data after container removal; bind mounts share host directories.
- Containers on the same Docker network reach each other by container name.
- Pin exact versions in FROM — unversioned tags are silent time bombs in production.
- Bind to 0.0.0.0 inside the container, not 127.0.0.1 — localhost inside a container is the container itself.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between a Docker image and a container?
- QWhy should you copy requirements.txt before copying your source code in a Dockerfile?
- QHow do containers on the same Docker network communicate?
- QWhat is the difference between docker stop and docker kill? When would you use each?
- QA container is running but you cannot reach it from the host on the mapped port. What are the possible causes?
- QWhat is the difference between a named volume, a bind mount, and a tmpfs mount? When would you use each?
- QYour docker build takes 5 minutes for a simple Python app. Walk me through how you would optimize it.
- QWhat does exit code 137 mean for a Docker container? How do you debug it?
Frequently Asked Questions
What is the difference between CMD and ENTRYPOINT in a Dockerfile?
ENTRYPOINT sets the executable that always runs — it cannot be overridden by docker run arguments. CMD provides default arguments to ENTRYPOINT or a default command if no ENTRYPOINT is set. Together: ENTRYPOINT is the binary, CMD is the default arguments. Use ENTRYPOINT for the main process of a container, CMD for configurable defaults.
How do you keep Docker images small?
Use slim or alpine base images (python:3.12-slim vs python:3.12). Use multi-stage builds to separate the build environment from the runtime image. Combine RUN commands to reduce layers: RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*. Copy only what the container needs — use .dockerignore.
What does exit code 137 mean for a Docker container?
Exit code 137 means the container was killed by signal 9 (SIGKILL), typically by the Linux OOM killer. The container exceeded its memory limit or the host ran out of memory. Debug with docker inspect to check OOMKilled status, then either increase the memory limit (--memory flag) or fix the memory leak in your application.
Does EXPOSE in a Dockerfile actually publish the port?
No. EXPOSE is documentation — it signals which port the application listens on but does not publish it. To actually make the port accessible from the host, use -p HOST:CONTAINER when running the container. EXPOSE is useful for documentation and for tools that auto-detect ports, but it has no runtime effect.
Can I modify a running Docker image?
No. Images are immutable. You can modify a running container's writable layer (add files, change config), but those changes are lost when the container is removed. To persist changes, either use volumes for data or create a new image with docker commit (not recommended for production — use a Dockerfile instead).
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.