=> [internal] load build definition from Dockerfile
=> CACHED [2/5] RUN apt-get update
=> [3/5] COPY package*.json ./
=> exporting to image
=> naming to docker.io/library/myapp:1.0
Note The dot at the end is the build context — Docker sends that entire directory to the daemon. Use .dockerignore to exclude node_modules and other heavy directories, or your builds will be painfully slow.
Note You cannot remove an image that has running or stopped containers using it. Stop and remove those containers first, or use -f to force removal.
remove imagedelete imageclean up imagesfree disk space
Prune Unused Images
syntax
docker image prune [options]
example
docker image prune
docker image prune -a --filter 'until=168h'
output
Total reclaimed space: 2.3GB
Note Without -a, this only removes dangling images (untagged). With -a, it removes ALL images not used by any container — this can delete base images you pulled but have not yet run. Use with caution.
prune imagesclean imagesremove unused imagesreclaim disk space
Note The output is JSON. Use --format with Go templates to extract specific fields like environment variables, labels, or exposed ports.
inspect imageimage detailsimage metadatashow image info
View Image Layer History
syntax
docker history <image>
example
docker history myapp:1.0docker history --no-trunc myapp:1.0
output
IMAGE CREATED BY SIZE
a1b2c3d4e5f6 CMD ["node" "server.js"] 0B
<missing> COPY . /app 45MB
<missing> RUN npm ci --omit=dev 85MB
Note This shows each layer and the command that created it. Great for diagnosing why an image is larger than expected — look for the biggest SIZE entries.
image historyimage layerslayer sizewhy is image big
Save & Load Images as Tarballs
syntax
docker save -o <file.tar> <image>
docker load -i <file.tar>
example
docker save -o myapp-v1.tar myapp:1.0# Transfer the file, then on the other machine:docker load -i myapp-v1.tar
output
Loaded image: myapp:1.0
Note Useful for air-gapped environments with no registry access. The tarball includes all layers, so it can be large. Consider piping through gzip: docker save myapp:1.0 | gzip > myapp.tar.gz
save imageexport imageload imagetransfer imageoffline imageair-gapped
docker run -d --name web-api -p 3000:3000 myapp:1.0docker run -it ubuntu:22.04 bash
output
e4f5a6b7c8d9...
Note docker run = docker create + docker start. The long hex string printed is the container ID. Use --name so you can refer to it by a human-readable name instead.
run containerstart container from imagelaunch containercreate and start
Start a Stopped Container
syntax
docker start <container>
example
docker start web-api
docker start -ai my-cli-tool
output
web-api
Note Unlike docker run, this does not create a new container — it restarts an existing one that was previously stopped. Use -ai to reattach stdin and see output.
Note Sends SIGTERM first and waits 10 seconds (default) before sending SIGKILL. Use -t to change the grace period. If your app does not handle SIGTERM, it will always take the full timeout before dying.
Note You cannot remove a running container without -f. The second example force-removes ALL containers — obviously destructive. Data in unnamed volumes is lost unless you use -v to also remove them intentionally.
remove containerdelete containerclean up containersdestroy container
List Containers
syntax
docker ps [options]
example
docker ps
docker ps -a
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
output
CONTAINER ID IMAGE STATUS PORTS NAMES
e4f5a6b7c8d9 myapp:1.0 Up 2 minutes 0.0.0.0:3000->3000/tcp web-api
Note Without -a you only see running containers. Stopped containers still exist and consume disk space until you docker rm them.
list containersshow running containerssee all containerscontainer status
Server listening on port 3000
GET /api/users 200 12ms
Note Use -f to follow in real time (like tail -f). Combine --tail and --since to avoid dumping thousands of lines. Logs persist until the container is removed.
Note The container must be running. Use -it for an interactive shell. The -u flag lets you override the user (e.g., run as root even if the image uses a non-root user).
exec into containershell into containerrun command in containerenter containerbash into container
Attach to a Running Container
syntax
docker attach <container>
example
docker attach web-api
Note This connects your terminal to the container's main process (PID 1). Pressing Ctrl+C sends SIGINT to that process and may stop the container. Use Ctrl+P, Ctrl+Q to detach without stopping. Prefer docker exec for most debugging scenarios.
attach containerconnect to containerview main process
Rename a Container
syntax
docker rename <old_name> <new_name>
example
docker rename web-api web-api-legacy
Note Works on running or stopped containers. Other containers referencing the old name via DNS in a user-defined network will no longer resolve it.
Note Blocks until the container stops and then prints the exit code. Exit code 0 means success. Useful in scripts that need to wait for a task container to finish before proceeding.
wait containerblock until doneexit codewait for finish
Show Port Mappings
syntax
docker port <container>
example
docker port web-api
output
3000/tcp -> 0.0.0.0:3000
8443/tcp -> 0.0.0.0:8443
Note Only shows ports explicitly published with -p at run time. Ports declared with EXPOSE in the Dockerfile are not listed here unless they were also published.
docker run -p <host_port>:<container_port> <image>
docker run -p <host_ip>:<host_port>:<container_port> <image>
example
docker run -d -p 8080:3000 myapp:1.0docker run -d -p 127.0.0.1:5432:5432 postgres:16
Note The format is always host:container. Binding to 127.0.0.1 restricts access to localhost only — critical for databases you do not want exposed on the network. Without an IP, Docker binds to 0.0.0.0 (all interfaces).
port mappingexpose portpublish portmap portforward port
Volume Mounts (-v)
syntax
docker run -v <host_path>:<container_path>[:<options>] <image>
docker run -v <volume_name>:<container_path> <image>
example
docker run -v ./src:/app/src myapp:1.0docker run -v pgdata:/var/lib/postgresql/data postgres:16docker run -v ./config.json:/app/config.json:ro myapp:1.0
Note Host paths starting with ./ or / create bind mounts. A plain name like pgdata creates or reuses a named volume. Append :ro for read-only. If the host path does not exist, Docker creates it as a directory (not a file), which often causes surprises.
mount volumebind mountshare filespersist datavolume mount
Environment Variables (-e)
syntax
docker run -e <KEY>=<value> <image>
docker run --env-file <file> <image>
example
docker run -e NODE_ENV=production -e DB_HOST=db myapp:1.0docker run --env-file .env myapp:1.0
Note Using --env-file keeps secrets out of your shell history and process list. Each line in the file should be KEY=value with no quotes needed around the value.
docker run -d --name web-api -p 3000:3000 myapp:1.0
output
e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3...
Note Runs the container in the background and prints the container ID. Use docker logs to see its output. Without -d, your terminal is attached and Ctrl+C stops the container.
run backgrounddetachdaemon modebackground container
Name a Container (--name)
syntax
docker run --name <name> <image>
example
docker run -d --name redis-cache redis:7-alpine
Note Names must be unique across all containers (running or stopped). If a previous container with the same name exists, remove it first with docker rm, or your run command will fail.
name containerset container namegive name
Specify Network (--network)
syntax
docker run --network <network_name> <image>
example
docker run -d --name web-api --network app-net myapp:1.0docker run -d --name db --network app-net postgres:16
Note Containers on the same user-defined network can reach each other by container name (e.g., web-api can connect to db:5432). The default bridge network does NOT provide this DNS — you must create a custom network.
docker run -d --restart unless-stopped --name web-api myapp:1.0docker run -d --restart on-failure:5 --name worker myapp:1.0
Note Policies: no (default), on-failure[:max-retries], always, unless-stopped. 'unless-stopped' will auto-start on daemon boot unless you explicitly stopped the container. 'always' restarts even after manual stops on daemon restart.
restart policyauto restartkeep runningrestart on crashrestart always
Memory Limit (--memory)
syntax
docker run --memory <limit> <image>
example
docker run -d --memory 512m --name web-api myapp:1.0docker run -d --memory 2g --name db postgres:16
Note Accepts b, k, m, g suffixes. If the container exceeds this limit, the kernel OOM killer will terminate it. Set --memory-swap to the same value to disable swap usage entirely.
memory limitram limitrestrict memoryout of memoryoom
CPU Limit (--cpus)
syntax
docker run --cpus <number> <image>
example
docker run -d --cpus 1.5 --name worker myapp:1.0docker run -d --cpus 0.5 --name background-job myapp:1.0
Note The value represents CPU cores (1.5 means one and a half cores). This is a soft limit via CFS scheduling, not a hard reservation. The container can still burst briefly if the host is idle.
cpu limitlimit cpurestrict cpucpu coresthrottle
Auto-Remove on Exit (--rm)
syntax
docker run --rm <image>
example
docker run --rm -it python:3.12 python -c 'print(2**100)'docker run --rm alpine:3.19 cat /etc/os-release
output
1267650600228229401496703205376
Note Automatically removes the container and its anonymous volumes when it exits. Perfect for one-off commands and throwaway shells. Cannot be combined with --restart.
auto removetemporary containerone-off commanddisposablecleanup after run
Interactive Mode (-it)
syntax
docker run -it <image> [command]
example
docker run -it ubuntu:22.04 bash
docker run -it node:20-alpine sh
output
root@e4f5a6b7c8d9:/#
Note -i keeps stdin open, -t allocates a pseudo-TTY. You almost always want both together. Without -t you get no prompt and no line editing. Without -i you cannot type into the shell.
FROM node:20-alpine
FROM python:3.12-slim AS builder
Note Every Dockerfile must start with FROM (or ARG before FROM). Always pin a specific tag — FROM node:latest today might be Node 20, but tomorrow it could be 22, breaking your build silently.
base imagefrom instructionparent imagestarting image
RUN — Execute Build Commands
syntax
RUN <command>
RUN ["executable", "arg1", "arg2"]
example
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
Note Each RUN creates a new image layer. Combine related commands with && to reduce layers and image size. Always clean up package manager caches in the same RUN statement, or the cache ends up in a previous layer and is never actually removed.
run commandinstall packagesexecute during buildbuild step
Note COPY only handles local files from the build context. Copy dependency manifests (package.json, requirements.txt) before the rest of the source code so that dependency-install layers are cached when only your application code changes.
copy filesadd files to imagecopy into containercopy source code
Note ADD can auto-extract tar archives and fetch URLs. However, COPY is preferred for simple file copies because ADD's extra behavior can be surprising. Using ADD for URL downloads also defeats layer caching — use RUN curl instead for better control.
add filesextract archivedownload urlcopy vs add
WORKDIR — Set Working Directory
syntax
WORKDIR /path/to/dir
example
WORKDIR /app
COPY . .
RUN npm ci
Note All subsequent RUN, CMD, COPY, and ENTRYPOINT instructions use this directory. WORKDIR creates the directory if it does not exist. Avoid using RUN cd /app — it only affects that single RUN layer.
working directoryset directorycd in dockerfilechange directory
Note There can only be one CMD. If you specify multiple, only the last one takes effect. CMD is overridden by any command you pass to docker run. Always prefer the exec form (JSON array) so the process runs as PID 1 and receives signals properly.
Note ENTRYPOINT sets the main executable. CMD then provides default arguments that users can override. Together: ENTRYPOINT ["python", "manage.py"] + CMD ["runserver"] means the user can run docker run myapp migrate to swap the subcommand.
entrypointmain processentry pointfixed commandentrypoint vs cmd
Note ENV values persist into the running container and into child images. If you only need a variable during build time (e.g., a version number), use ARG instead to avoid leaking it into the final image.
environment variableset env in dockerfileenv instructionbuild-time variable
ARG — Build-Time Variables
syntax
ARG <name>[=<default_value>]
example
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
ARG BUILD_DATE
LABEL build-date=$BUILD_DATE
Note ARG values are only available during build (not at runtime). Pass them with docker build --build-arg NODE_VERSION=22. ARG before FROM is only in scope for the FROM line itself — redeclare it after FROM if needed later.
Note EXPOSE is documentation only — it does NOT publish the port. You still need -p at runtime. Think of it as a note to anyone reading the Dockerfile about which ports the application listens on.
Note Creates an anonymous volume at that path when no explicit mount is provided. This prevents data from growing the container's writable layer. However, anonymous volumes are hard to manage — prefer named volumes via docker run -v.
volume instructiondeclare volumepersistent storagemount point
USER — Set Runtime User
syntax
USER <username>[:<group>]
example
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
Note All subsequent RUN, CMD, and ENTRYPOINT instructions run as this user. Running as root in production is a security risk — always create and switch to a non-root user for the final stage.
set usernon-rootrun as usersecurity userdrop privileges
Note Labels are key-value metadata stored in the image. They show up in docker inspect and can be used as filters: docker images --filter label=version=2.1. Use the OCI standard keys (org.opencontainers.image.*) for interoperability.
Note Docker marks the container as healthy, unhealthy, or starting based on exit codes. Orchestrators use this to decide when to route traffic or restart containers. Install curl or wget in your image if you use HTTP checks.
FROM <image> AS <stage>
...
FROM <image>
COPY --from=<stage> <src> <dest>
example
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Note The final image only contains the layers from the last FROM stage. Build tools, dev dependencies, and source code from earlier stages are discarded, dramatically reducing image size. A Go API might go from 1.2GB (with SDK) to 15MB (static binary on scratch).
Note Place this file next to your Dockerfile. Without it, Docker sends everything in the build context to the daemon, including node_modules (hundreds of MB), .git history, and possibly secrets in .env files. This is one of the highest-impact optimizations you can make.
dockerignoreexclude filesbuild contextignore filesspeed up build
Note The file can be named compose.yaml (preferred), compose.yml, docker-compose.yaml, or docker-compose.yml. Compose V2 is the current standard — use 'docker compose' (no hyphen) instead of the legacy 'docker-compose' binary.
Note Named volumes declared in the top-level volumes: key persist across restarts and compose down. Bind mounts (host paths) are useful for development but should not be used for database storage in production.
compose volumespersist datanamed volume composemount in compose
Note Compose creates a default network automatically, so all services can already talk to each other. Define custom networks when you want to isolate groups — here, web cannot reach db directly.
compose networksisolate servicesnetwork segmentationservice communication
Note Always quote port mappings in YAML — values like 80:80 can be misinterpreted as a base-60 number by the YAML parser. Binding debug ports to 127.0.0.1 prevents accidental exposure on the network.
compose portsexpose port composeport mapping composepublish port
services:
api:
environment:
NODE_ENV: production
DB_HOST: db
env_file:
- .env.local
Note Compose automatically reads a .env file in the same directory for variable substitution in the compose file itself (e.g., image: myapp:${VERSION}). The env_file key loads variables into the container's environment.
Note Plain depends_on only waits for the container to start, NOT for the application inside to be ready. Use condition: service_healthy with a healthcheck to wait until the database actually accepts connections.
depends onservice dependencystartup orderwait for serviceservice healthy
Build Configuration
syntax
services:
api:
build:
context: <path>
dockerfile: <file>
args:
KEY: value
Note Setting both build and image means Compose builds and tags the result with that image name. The context path is relative to the compose file location.
Note Services with profiles are not started by default. They only launch when you explicitly activate the profile. Great for optional tools like database GUIs, debug utilities, or test runners.
docker compose up [options]
docker compose down [options]
example
docker compose up -d
docker compose up -d --builddocker compose down
docker compose down -v
output
✔ Container myproject-db-1 Started
✔ Container myproject-api-1 Started
Note up -d runs in the background. Add --build to rebuild images before starting. down stops and removes containers and default networks. down -v ALSO removes named volumes — this destroys your database data permanently.
Note sync copies changed files into the container without rebuilding. rebuild triggers a full image rebuild when the specified file changes (e.g., when you add a dependency). This replaces many custom file-watching setups.
Note Compose automatically merges compose.override.yaml on top of compose.yaml. Use -f flags for other environments: docker compose -f compose.yaml -f compose.prod.yaml up. This keeps your base config DRY.
compose overridemultiple compose filesdev vs prodcompose mergeenvironment config
docker volume create <name>
docker run -v <name>:<container_path> <image>
example
docker volume create pgdata
docker run -d -v pgdata:/var/lib/postgresql/data postgres:16
output
pgdata
Note Named volumes are managed by Docker and stored under /var/lib/docker/volumes/ on Linux. They persist independently of containers and survive docker rm. This is the recommended way to persist database data.
named volumecreate volumepersist datadatabase storage
Bind Mounts
syntax
docker run -v <host_path>:<container_path>[:<options>] <image>
docker run --mount type=bind,source=<path>,target=<path> <image>
example
docker run -v $(pwd)/src:/app/src myapp:1.0docker run --mount type=bind,source=$(pwd)/config,target=/app/config,readonly myapp:1.0
Note Bind mounts give the container direct access to host files — changes are reflected immediately in both directions. On macOS and Windows, bind mount performance can be noticeably slow for large node_modules directories.
bind mountmount host directoryshare folderlocal files in container
tmpfs Mounts (RAM Only)
syntax
docker run --tmpfs <container_path>[:<options>] <image>docker run --mount type=tmpfs,target=<path> <image>
example
docker run -d --tmpfs /tmp:rw,noexec,size=100m myapp:1.0docker run -d --mount type=tmpfs,target=/app/cache,tmpfs-size=67108864 myapp:1.0
Note Data is stored in memory and disappears when the container stops. Use for sensitive data (secrets, tokens) that should never be written to disk, or for scratch space where speed matters more than persistence.
Note The Mountpoint shows where the data lives on the host filesystem. On Docker Desktop (macOS/Windows), this path is inside the Linux VM, not directly accessible from your host.
inspect volumevolume detailsvolume locationwhere is data stored
DRIVER VOLUME NAME
local pgdata
local redis-data
local a1b2c3d4e5f6...
Note docker volume prune removes ALL volumes not used by any container. This destroys data permanently with no undo. The long hex-named volumes are anonymous volumes — usually safe to remove, but verify first.
list volumesremove volumedelete volumeprune volumesclean up volumes
Backup a Volume
syntax
docker run --rm -v <volume>:/source -v $(pwd):/backup <image> tar czf /backup/<file>.tar.gz -C /source .
example
docker run --rm \
-v pgdata:/source:ro \
-v $(pwd):/backup \
alpine:3.19 \
tar czf /backup/pgdata-backup.tar.gz -C /source .
Note This spins up a temporary Alpine container that mounts the volume read-only and your current directory, then creates a compressed tar archive. To restore, reverse the process by extracting the tar into a new volume.
docker run -v <container_path> <image>
# or VOLUME in Dockerfile
example
docker run -d -v /var/cache myapp:1.0# Creates an anonymous volume with a random hex name
Note Anonymous volumes get a random hash name and are easy to lose track of. They accumulate over time and waste disk space. Prefer named volumes so you can identify and manage your data. Use docker volume prune periodically to clean up orphans.
docker run <image>
# Container joins the default bridge network
example
docker run -d --name web nginx:alpinedocker run -d --name api myapp:1.0# These are on the default bridge but cannot resolve each other by name
Note The default bridge network assigns each container an IP, but DNS resolution by container name does NOT work. You must use IP addresses or create a custom bridge network for name-based communication.
docker run -d --network host myapp:1.0# App is directly accessible on host port 3000 — no -p needed
Note The container shares the host's network stack directly. No port mapping is needed or possible. This gives the best network performance but zero isolation. Only works on Linux — Docker Desktop on macOS/Windows does not support host networking.
docker run --network none --rm alpine:3.19 ping google.com# ping: bad address 'google.com'
Note Completely disables networking. The container only has a loopback interface. Use this for batch processing or security-sensitive workloads that should never make network calls.
no networkdisable networkisolated containernetwork none
Note Custom bridge networks provide automatic DNS so containers can find each other by name. This is almost always what you want for multi-container setups outside of Compose.
create networkcustom networkuser-defined networkcontainer dns
Note A container can be connected to multiple networks simultaneously. This is useful for gateway containers that need to communicate with two isolated groups of services.
connect to networkdisconnect networkjoin networkmulti-network
Container-to-Container Communication
syntax
# On the same custom network, use container names as hostnames
example
docker network create app-net
docker run -d --name db --network app-net postgres:16docker run -d --name api --network app-net -e DB_HOST=db myapp:1.0# api can connect to db:5432 by name
Note DNS resolution by container name only works on user-defined networks, not on the default bridge. This is the number one networking gotcha for Docker beginners. In Compose, services automatically get DNS names matching their service key.
container to containerdns resolutionservice discoveryconnect by name
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
b2c3d4e5f6a7 app-net bridge local
c3d4e5f6a7b8 host host local
d4e5f6a7b8c9 none null local
Note Inspect shows the subnet, gateway, and which containers are connected. Useful for debugging connectivity issues between containers.
list networksinspect networkshow networksnetwork details
Publishing Ports to the Host
syntax
docker run -p <host_port>:<container_port> <image>
docker run -P <image>
example
docker run -d -p 8080:80 -p 8443:443 nginx:alpine
docker run -d -P nginx:alpine
Note -P (uppercase) publishes ALL exposed ports to random high-numbered host ports — check the mapping with docker port. Docker's port forwarding bypasses iptables/firewall rules on Linux, which can unexpectedly expose services. Use 127.0.0.1:port:port to restrict to localhost.
publish portport forwardingexpose to hostrandom portfirewall bypass
Note Without a registry argument, Docker logs into Docker Hub. Credentials are stored in ~/.docker/config.json — often in plain text unless you configure a credential helper. Set up a credential store for production machines.
docker loginauthenticateregistry authsign in
Tag & Push Workflow
syntax
docker tag <local_image> <registry>/<repo>:<tag>
docker push <registry>/<repo>:<tag>
example
docker build -t myapp:1.0 .
docker tag myapp:1.0 ghcr.io/myorg/myapp:1.0docker tag myapp:1.0 ghcr.io/myorg/myapp:latest
docker push ghcr.io/myorg/myapp:1.0docker push ghcr.io/myorg/myapp:latest
Note Always push a specific version tag alongside latest. This way, deployments can pin to a known version while latest serves as a convenience pointer for development.
push workflowtag and pushpublish to registryrelease image
Note Common patterns: semver (2.1.0), major.minor (2.1), variant suffix (-alpine, -slim), git-based (main-abc1234, sha-abc1234). Never rely on :latest in production — it is a mutable tag and gives you no way to reproduce a specific build.
tag conventionsnaming imagesversion tagsimage tagstagging strategy
Private Registry Setup
syntax
docker run -d -p 5000:5000--name registry registry:2
example
docker run -d -p 5000:5000--restart always --name registry registry:2docker tag myapp:1.0 localhost:5000/myapp:1.0docker push localhost:5000/myapp:1.0docker pull localhost:5000/myapp:1.0
Note This runs a minimal private registry. For production, add TLS certificates and authentication. Docker refuses to push to non-HTTPS registries by default — add the registry to the insecure-registries list in daemon.json only for local testing.
Note Builds images for multiple architectures in one command. The --push flag is required because multi-platform images cannot be loaded into the local daemon (they are a manifest list, not a single image). You need QEMU emulation configured for non-native platforms.
multi-platformbuildxarm64cross-compilemulti-archamd64 arm
Note Shows which platforms an image supports without pulling the entire image. Useful for verifying that your multi-platform build actually produced the architectures you expected.
Server started on port 3000
GET /api/health 200 2ms
POST /api/users 201 45ms
Note Combine --tail to avoid the initial flood and --since to only see recent entries. Pressing Ctrl+C stops following but does not affect the container.
Note Alpine-based images have sh but not bash. If the container has no shell at all (distroless or scratch), you can use docker debug (Docker Desktop) or copy a static busybox binary in via docker cp.
Note Returns comprehensive JSON with networking, mounts, environment, state, and configuration. The --format flag with Go templates extracts exactly what you need without piping through jq.
Note Shows the process tree from the host's perspective. Useful for confirming which processes are actually running and spotting zombie or unexpected processes.
container processestopps in containerrunning processes
Note Streams lifecycle events (create, start, die, destroy, OOM) in real time. Filter by container, image, or event type. Exit code 137 means OOM killed (128 + signal 9).
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 15 5 4.2GB 2.8GB (66%)
Containers 8 3 125MB 95MB (76%)
Volumes 6 3 1.1GB 400MB (36%)
Build Cache - - 850MB 850MB
Note Quick overview of what is consuming disk. Add -v for a per-item breakdown. The RECLAIMABLE column shows how much you can free with prune commands.
disk usagedocker spacewhat is using diskstorage breakdownreclaim space
System-Wide Cleanup
syntax
docker system prune [options]
example
docker system prune
docker system prune -a --volumes
output
Total reclaimed space: 5.7GB
Note Without flags: removes stopped containers, unused networks, dangling images, and build cache. With -a: also removes all unused images (not just dangling). With --volumes: also removes unused volumes. This is the nuclear option — it can delete data you care about.
system pruneclean everythingfree spacedocker cleanupreclaim disk
Note Works on both running and stopped containers. Useful for extracting crash logs or injecting a config file for debugging. Changes via cp do not persist across container recreation — use volumes for anything permanent.
copy filesdocker cpextract fileinject fileget logs from container
See Filesystem Changes
syntax
docker diff <container>
example
docker diff web-api
output
C /app
A /app/logs/error.log
A /tmp/cache-abc123
D /app/config.bak
Note Shows which files were Added (A), Changed (C), or Deleted (D) compared to the image. Handy for understanding what the running process has modified on disk.
# Copy dependency files first, install, then copy source
example
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# NOT:# COPY . .# RUN npm ci
Note Docker caches each layer. If you COPY everything first, changing a single source file invalidates the npm ci cache. By copying package files separately, the install layer is cached until dependencies actually change — saving minutes on every build.
FROM <image>:<version>-alpine
FROM <image>:<version>-slim
example
# Instead of:
FROM node:20# ~1GB# Use:
FROM node:20-alpine # ~130MB
FROM python:3.12-slim # ~150MB vs 900MB for full
Note Alpine images use musl libc instead of glibc, which can cause issues with some native Node.js modules or Python C extensions. Test thoroughly. Debian slim is a safer middle ground if Alpine causes problems.
small imagealpineslim imagereduce sizeminimal base image
Run as Non-Root User
syntax
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
example
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
CMD ["node", "server.js"]
Note Node images ship with a built-in 'node' user. Many images include a non-root user — check the docs before creating your own. Running as root inside a container means a container escape could grant host-level root access.
non-rootsecurityrun as userdrop rootcontainer security
Note Without .dockerignore, a COPY . . sends everything to the daemon including .git (often 100MB+), node_modules, environment files with secrets, and test coverage reports. Builds become slow and images become bloated.
dockerignoreignore filesexclude from buildbuild context size
Note Use wget instead of curl in Alpine images (wget is built in, curl is not). The --start-period gives slow-starting apps time to initialize before health checks count against them.
FROM <image>:<specific_version>
RUN apt-get install -y <package>=<version>
example
FROM node:20.11.1-alpine3.19
RUN apk add --no-cache dumb-init=1.2.5-r3
Note Unpinned versions mean your builds are not reproducible. node:20-alpine could be 20.10 today and 20.12 tomorrow. Pin the full version for critical images. Even apk/apt packages should be pinned in production Dockerfiles.
FROM <image> AS build
...
FROM <image>
COPY --from=build ...
example
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /app/server
CMD ["/app/server"]
Note The Go SDK (800MB+) stays in the build stage only. The final image is just the static binary on distroless (~2MB). This pattern works for any compiled language and even for frontend builds where you need Node to compile but only serve static files.
✗ HIGH 2 vulnerabilities
✗ MEDIUM 5 vulnerabilities
✓ LOW 3 vulnerabilities (fixes available)
Note Run scans in CI before pushing images. The --only-fixed flag filters to vulnerabilities that have a patched version available, so you can focus on actionable fixes rather than the full list.
# Good — single layer, cache cleaned:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Bad — three layers, cache stuck in layer 1:# RUN apt-get update# RUN apt-get install -y curl# RUN rm -rf /var/lib/apt/lists/*
Note Separate RUN statements each create a layer. The rm in a later layer does not reduce the image size because the deleted files still exist in a previous layer. Always install and clean up in the same RUN command.
combine runreduce layerssmaller imagelayer optimizationcleanup in same layer
FROM node:20-alpine
RUN apk add --no-cache dumb-init
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
Note Node.js and Python do not handle signals well as PID 1. Without an init process, SIGTERM from docker stop is not forwarded to your app, and the container takes the full 10-second timeout before being killed. dumb-init or tini fixes this.
# Problem:
FROM node:20
CMD ["node", "server.js"]
# Runs as root by default
example
# Fix:
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
CMD ["node", "server.js"]
Note Most base images default to root. This means if an attacker exploits your application, they have root inside the container — and potentially on the host via privilege escalation. Always add a USER instruction before CMD.
running as rootsecurity mistakeroot containerprivilege escalation
Bloated Image Size
syntax
# Problem:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 build-essential ...
COPY . .
# 1.5GB image for a simple Python app
example
# Fix:
FROM python:3.12-slim AS build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
FROM python:3.12-slim
COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . /app
CMD ["python", "/app/main.py"]
Note Common culprits: using the full Ubuntu/Debian base, leaving build tools installed, not cleaning apt/pip caches, copying node_modules into the image. Use docker history to identify the largest layers.
fat imageimage too largereduce sizebloated imageimage diet
Not Pinning Image Versions
syntax
# Problem:
FROM node:latest
FROM python
FROM nginx
example
# Fix — pin to specific versions:
FROM node:20.11.1-alpine3.19
FROM python:3.12.2-slim-bookworm
FROM nginx:1.25.4-alpine
Note An image tag like :latest or :20 can change at any time. Your build might work today and fail tomorrow because the base image was updated. In CI, a non-reproducible build means you cannot investigate a production issue with the exact same image.
Note Every time you rebuild with the same tag, the old image loses its tag and becomes <none>:<none>. These pile up fast. Volumes from removed containers also linger. Schedule regular prune commands or add them to your CI cleanup steps.
# Problem:docker run -d -p 3000:3000--name api-v1 myapp:1.0docker run -d -p 3000:3000--name api-v2 myapp:2.0# Error: port is already allocated# Fix — use different host ports:docker run -d -p 3001:3000--name api-v2 myapp:2.0
output
Error response from daemon: driver failed: Bind for 0.0.0.0:3000 failed: port is already allocated
Note Two containers cannot bind the same host port. Check what is using a port with lsof -i :3000 (macOS/Linux) or docker ps --format to see existing mappings. Remember that host services (like a locally running Node process) also count.
port conflictport already in useaddress in useport collision
Accidental Build Cache Invalidation
syntax
# Problem — COPY . . before RUN npm ci
COPY . .
RUN npm ci
example
# Every source code change re-runs npm ci (~30-60 seconds).# Fix — copy manifests first:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Note Docker invalidates a layer's cache when any input changes. A broad COPY . . changes whenever ANY file in your project changes, nuking the cache for all subsequent layers. Order Dockerfile instructions from least-frequently to most-frequently changing.
cache invalidationslow buildcache missbuild optimizationlayer order
# Use COPY for plain file copies (clear intent):
COPY package.json ./
COPY src/ ./src/
# Only use ADD when you specifically want extraction:
ADD assets.tar.gz /app/assets/
Note ADD auto-extracts tar archives and can fetch URLs, which is rarely what you want. COPY does exactly one thing — copies files. Using COPY makes your Dockerfile easier to reason about and avoids accidental extraction of files you wanted to keep as archives.
copy vs addadd instructionwhen to use addcopy preferred
Baking Secrets into Images
syntax
# NEVER do this:
ENV DATABASE_URL=postgres://user:pass@host/db
COPY .env /app/.env
example
# Bad — secret is in the image layer history:
ENV API_KEY=sk-abc123...
# Fix — pass secrets at runtime:docker run -e API_KEY=sk-abc123 myapp:1.0docker run --env-file .env myapp:1.0# For build-time secrets (e.g., private npm token):docker build --secret id=npmrc,src=.npmrc .# In Dockerfile:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Note ENV values and COPYed files are permanently visible via docker history and docker inspect. Even if you delete them in a later layer, they exist in the previous layer. Use --secret mounts for build-time secrets and runtime env vars or secret managers for runtime secrets.
secrets in imageleaked credentialsenv secretsbuild secretssecurity mistake