Getting Started
Basic Commands
docker run creates and starts a container. -d runs in background, -p maps host port to container port. pull downloads images from a registry (Docker Hub by default).
# check version
docker --version
# pull an image
docker pull nginx:latest
# run a container
docker run -d --name mynginx -p 8080:80 nginx
# list running containers
docker ps
# list all containers (including stopped)
docker ps -a
# stop and remove a container
docker stop mynginx
docker rm mynginx
# list images
docker imagesContainer Lifecycle
create + start is what run does under the hood. stop sends SIGTERM for graceful shutdown; kill sends SIGKILL immediately. pause uses cgroup freezer — useful for snapshots without stopping the process.
# create a container without starting it
docker create --name web nginx
# start a created (or stopped) container
docker start web
# stop gracefully (SIGTERM then SIGKILL after 10s)
docker stop web
# force kill immediately
docker kill web
# restart a container
docker restart web
# pause/resume (freezes process with cgroups)
docker pause web
docker unpause webImage Basics
Images are read-only layers. rmi fails if a container still references the image — remove the container first. history shows each instruction that created a layer and its size, useful for optimizing builds.
# list local images
docker images
docker image ls
# remove an image
docker rmi nginx:latest
docker image rm nginx
# remove all dangling (untagged) images
docker image prune
# remove all unused images
docker image prune -a
# show image history (each layer)
docker history nginx:latestSystem Information
docker info shows daemon config: storage driver, runtime, registries, swarm status. system df reveals reclaimable space — dangling images and stopped containers waste disk. The --format flag uses Go templates for structured output.
# docker system-wide info
docker info
docker version
# disk usage breakdown (images, containers, volumes)
docker system df
# detailed disk usage with reclaimable space
docker system df -v
# running containers count
docker ps -q | wc -l
# show docker root directory and storage driver
docker info --format '{{.DockerRootDir}} {{.Driver}}'Cleanup & Prune
prune safely removes stopped/dangling resources. system prune -a also removes images not referenced by any container. Adding --volumes deletes volumes not used by any container — irreversible data loss, use with care in production.
# remove stopped containers
docker container prune
# remove dangling images
docker image prune
# remove unused volumes (CAUTION: data loss)
docker volume prune
# remove unused networks
docker network prune
# remove everything unused at once
docker system prune
# also remove volumes (very aggressive)
docker system prune -a --volumesGetting Help
Docker groups commands into management namespaces (docker image, docker container, docker network, docker volume). Both old (docker rmi) and new (docker image rm) forms work. --help on any subcommand shows flags and usage.
# list all commands
docker --help
docker help
# help for a specific command
docker run --help
docker image --help
# list management commands (grouped)
docker image --help
docker container --help
docker network --help
# show daemon config
docker info
# command reference online
# https://docs.docker.com/reference/Image Management
Pulling Images
Tags are mutable labels — the same tag can point to different images over time. For reproducible deploys, pin by digest (@sha256:...). Pulling by --platform lets you fetch an image for a different architecture, useful for ARM builds on x86 hosts.
# pull latest tag
docker pull nginx
# pull specific tag
docker pull nginx:1.25-alpine
# pull all tags of a repository
docker pull -a alpine
# pull from a different registry
docker pull ghcr.io/user/repo:tag
# pull by digest (immutable, reproducible)
docker pull nginx@sha256:abc123...
# pull multiple platforms (multi-arch)
docker pull --platform linux/arm64 nginxBuilding Images
The final . is the build context — files sent to the daemon. Keep it small with .dockerignore to speed builds. --no-cache forces fresh layers when debugging. BuildKit enables multi-stage caching, SSH mounts, and secret mounting.
# build from Dockerfile in current dir, tag it
docker build -t myapp:1.0 .
# build with a different Dockerfile name
docker build -f Dockerfile.prod -t myapp:prod .
# build with build args
docker build --build-arg VERSION=1.2.3 -t myapp .
# build with no cache
docker build --no-cache -t myapp .
# build for a different platform
docker build --platform linux/arm64 -t myapp:arm .
# build using BuildKit (advanced features)
DOCKER_BUILDKIT=1 docker build -t myapp .Tagging Images
Tags are aliases pointing to the same image ID. A common workflow builds with a version tag then adds latest and major-version tags. Tagging with a registry URL prepares the image for a push to that registry.
# tag a local image
docker tag myapp:1.0 myapp:latest
# tag for pushing to Docker Hub
docker tag myapp:1.0 username/myapp:1.0
# tag for a private registry
docker tag myapp:1.0 registry.local:5000/myapp:1.0
# tag with multiple tags at once
docker tag myapp:1.0 myapp:stable
docker tag myapp:1.0 myapp:1
# tag by digest reference
docker tag myapp@sha256:abc... myapp:pinnedPushing Images
You must tag the image with the registry destination before pushing. push uploads only layers not already present in the registry. For private registries with self-signed certs, configure the daemon in /etc/docker/daemon.json with insecure-registries.
# login to Docker Hub
docker login
# login to a private registry
docker login registry.local:5000 -u user -p pass
# push to Docker Hub
docker push username/myapp:1.0
# push all tags in a repository
docker push -a username/myapp
# push to private registry
docker push registry.local:5000/myapp:1.0
# logout when done
docker logoutInspecting Images
inspect returns detailed JSON: architecture, OS, layers, env, entrypoint, config. history reveals each layer's command and size — the foundation of image optimization. manifest inspect checks a remote registry without pulling.
# low-level image metadata (JSON)
docker inspect nginx:latest
# specific field via Go template
docker inspect --format '{{.Arch}} {{.Os}}' nginx
# show image layers and sizes
docker history nginx:latest
# no-trunc to see full command for each layer
docker history --no-trunc nginx:latest
# inspect an image in a registry (manifest)
docker manifest inspect nginx:latestSaving & Loading Images
save/load preserves image layers and history — the right choice for transferring images offline. export/import flattens to a single layer, losing history. For online transfer, prefer registry push/pull over tar files.
# save an image to a tar file
docker save -o myapp.tar myapp:1.0
# save multiple images
docker save -o bundle.tar myapp:1.0 nginx:latest
# load an image from a tar file
docker load -i myapp.tar
# pipe between hosts (no intermediate file)
docker save myapp:1.0 | gzip | ssh host 'gunzip | docker load'
# export a container filesystem (not an image)
docker export mycontainer | gzip > fs.tar.gzContainer Management
Running Containers
-it combines -i (keep stdin open) and -t (allocate a tty) for interactive sessions. --rm cleans up after exit — ideal for one-off commands. --restart unless-stopped survives reboots but respects manual stops.
# run in background with a name and port mapping
docker run -d --name web -p 8080:80 nginx
# run interactively with a terminal
docker run -it --name shell alpine sh
# run and auto-remove on exit
docker run --rm alpine echo hello
# run with a volume
docker run -d -v mydata:/data alpine
# run with environment variables
docker run -d -e MYSQL_ROOT_PASSWORD=secret mysql
# run with a restart policy
docker run -d --restart unless-stopped nginxExecuting Commands (exec)
exec runs a new process inside an already-running container — the original entrypoint keeps running. Use it for debugging, admin tasks, or running one-off scripts. The default user is the image's USER; override with -u.
# open an interactive shell in a running container
docker exec -it web sh
# run a command and return
docker exec web ls /usr/share/nginx/html
# run as a specific user
docker exec -u root web whoami
# set environment variables for the command
docker exec -e DEBUG=1 web env
# run with a working directory
docker exec -w /tmp web pwd
# run as detached (rarely useful)
docker exec -d web touch /tmp/markerViewing Logs
logs shows stdout/stderr captured by the container's logging driver. Applications should log to stdout, not files, to integrate with docker logs. --since/--until accept RFC3339 timestamps or Go durations (30m, 2h).
# follow (tail) logs
docker logs -f web
# show last 100 lines
docker logs --tail 100 web
# show logs since a timestamp
docker logs --since 2024-01-01T00:00:00 web
# show logs from the last 30 minutes
docker logs --since 30m web
# add timestamps to each line
docker logs -t web
# show logs for a specific timeframe
docker logs --since 10m --until 5m webContainer Inspection
inspect returns rich JSON covering config, state, network, and mounts. Go templates extract specific fields — extremely useful for scripting. The LogPath reveals where the json-file driver stores logs on the host.
# full JSON metadata
docker inspect web
# container IP address
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
# container log path (json-file driver)
docker inspect --format '{{.LogPath}}' web
# mounts and volumes
docker inspect --format '{{json .Mounts}}' web
# container state (running, exited, etc.)
docker inspect --format '{{.State.Status}} {{.State.ExitCode}}' web
# all container ports
docker inspect --format '{{json .NetworkSettings.Ports}}' webStarting, Stopping & Restarting
start resumes a stopped container with its original config. update modifies a running container's constraints and restart policy without recreating it — useful for tightening resource limits in production.
# start a stopped container
docker start web
# stop with a custom timeout (seconds)
docker stop -t 30 web
# restart (keeps container intact)
docker restart web
# rename a container
docker rename web webserver
# update container config (e.g. restart policy)
docker update --restart unless-stopped web
# pause/resume (cgroup freezer)
docker pause web
docker unpause webCopying Files
cp works in both directions and on stopped containers — handy for injecting hotfixes or extracting logs. It is not designed for high-frequency sync; use a bind mount or volume for development workflows. The tar+exec trick streams large trees efficiently.
# copy a file from host to container
docker cp file.txt web:/tmp/
# copy from container to host
docker cp web:/etc/nginx/nginx.conf ./nginx.conf
# copy a directory
docker cp ./app web:/usr/src/app
# copy with archive mode (preserves perms)
docker cp -a ./config web:/etc/app
# copy to a stopped container (works too)
docker cp script.sh web:/script.sh
# tar stream from host to container stdin
docker exec -i web tar xzf - -C /app < app.tar.gzDockerfile Basics
FROM — Base Image
FROM is the first instruction and defines the base layer. Choose minimal bases (alpine, distroless, scratch) to reduce attack surface and size. Pinning by digest guarantees reproducible builds since tags can change.
# use an official base image
FROM ubuntu:22.04
# pin a specific digest for reproducibility
FROM ubuntu@sha256:abc123def...
# multi-arch build (buildx picks the host arch)
FROM --platform=$BUILDPLATFORM golang:1.21 AS builder
# use a minimal image
FROM alpine:3.18
# scratch = empty image, for static binaries
FROM scratch
# use a previous build stage
FROM builder AS finalRUN — Execute Commands
Each RUN creates a layer. Combine related commands with && and clean up in the same layer to keep images small. The exec form (JSON array) does not invoke a shell, so no variable expansion or chaining — but it handles signals correctly.
# install packages
RUN apt-get update && apt-get install -y curl
# clean up in the same layer to reduce size
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# combine commands with && to reduce layers
RUN mkdir -p /app/config && chown 1000:1000 /app/config
# use exec form (preferred) to ensure signals
RUN ["npm", "install"]
# use a shell for variables and chaining
RUN VERSION=1.0 && wget https://example.com/v${VERSION}.tar.gzCOPY & ADD
COPY is preferred for local files — it is explicit and predictable. ADD adds magic: URL fetching and auto-extraction of tar/zip archives. This magic can surprise you; reach for ADD only when you need extraction. Both create a layer.
# copy a file into the image
COPY package.json /app/
# copy a directory
COPY src/ /app/src/
# copy with ownership
COPY --chown=node:node package.json /app/
# copy from a previous build stage
COPY --from=builder /app/dist /app/dist
# ADD can fetch URLs and extract archives
ADD https://example.com/app.tar.gz /tmp/
# ADD extracts local tar archives automatically
ADD app.tar.gz /usr/src/WORKDIR
WORKDIR sets the working directory for subsequent instructions and the container's default. It creates the directory if missing. Always prefer WORKDIR over cd in RUN — cd does not persist across layers. Use absolute paths for clarity.
# set the working directory
WORKDIR /usr/src/app
# subsequent RUN, CMD, COPY use this dir
COPY . .
RUN npm install
# WORKDIR creates the directory if it does not exist
WORKDIR /opt/data/logs
# use a relative path (relative to previous WORKDIR)
WORKDIR sub
# avoid this anti-pattern (use absolute paths)
WORKDIR /app
RUN cd /app && do_something # cd is lost after RUNUSER
USER sets the user (and optionally group) for RUN, CMD, and ENTRYPOINT. Running as non-root is a key security best practice. The user must exist in the image. Numeric UIDs are portable across images and avoid name lookup issues.
# create a non-root user and switch to it
RUN groupadd -r app && useradd -r -g app appuser
USER appuser
# run subsequent instructions as this user
RUN whoami # appuser
# use a numeric UID for portability
USER 1000
# USER in compose/runtime overrides this
USER 1000:1000
# common pattern: node image ships a 'node' user
FROM node:20-alpine
USER node.dockerignore
.dockerignore reduces the build context sent to the daemon — faster builds, smaller context, and prevents secrets like .env from leaking into image layers. It uses similar syntax to .gitignore. Always exclude node_modules and build artifacts that are recreated during the build.
# .dockerignore — exclude files from the build context
# version control
.git
.gitignore
# dependencies (fetched inside the image)
node_modules
npm-debug.log
# build artifacts
dist
build
*.log
# local env files (NEVER bake secrets)
.env
.env.local
# docker files themselves
Dockerfile
docker-compose*.yml
# OS files
.DS_Store
Thumbs.dbDockerfile Advanced
CMD vs ENTRYPOINT
ENTRYPOINT defines the executable; CMD provides default arguments that can be overridden. Use exec form (JSON array) so the process receives signals directly — shell form wraps the command in /bin/sh -c, breaking graceful shutdown. A common pattern: ENTRYPOINT script + CMD default args.
# CMD: default command, easily overridden
CMD ["nginx", "-g", "daemon off;"]
# override at runtime
# docker run myimage nginx -v
# ENTRYPOINT: fixed command, CMD becomes args
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
# runtime args append to ENTRYPOINT
# docker run myimage -v -> nginx -v
# shell form (avoid — no signal handling)
CMD nginx -g "daemon off;"
# script as entrypoint that accepts args
ENTRYPOINT ["./entrypoint.sh"]
CMD ["--help"]ENV — Environment Variables
ENV creates image-level variables visible at build and runtime. Use ARG for build-only values to avoid leaking config into the final image. In exec-form CMD/ENTRYPOINT, ${VAR} is not expanded by the shell — wrap with sh -c to expand.
# set a persistent env var (baked into image)
ENV NODE_ENV=production
ENV PATH=/app/node_modules/.bin:$PATH
# use in subsequent RUN instructions
ENV VERSION=1.2.3
RUN wget https://example.com/v${VERSION}.tar.gz
# multiple in one line
ENV NODE_ENV=production PORT=3000 LOG_LEVEL=info
# override at runtime
# docker run -e NODE_ENV=development myapp
# view env vars of a container
docker exec web env
# reference env vars in CMD
ENV APP_PORT=3000
CMD ["sh", "-c", "node server.js ${APP_PORT}"]ARG — Build Arguments
ARG values exist only during the build of the stage that declares them. They do not persist into the runtime container — use ENV to carry a value to runtime. ARGs declared before FROM are 'global' but must be re-declared in each stage to use there.
# declare a build argument with a default
ARG VERSION=latest
FROM base:${VERSION}
# use ARG only during build (not at runtime)
ARG NODE_ENV=production
RUN echo "Building for ${NODE_ENV}"
# multi-stage: ARG must be redeclared per stage
ARG VERSION
FROM base:${VERSION}
ARG VERSION
RUN echo ${VERSION}
# pass at build time
# docker build --build-arg VERSION=1.0 .
# ARG not available in CMD (use ENV to pass through)
ARG PORT=3000
ENV PORT=${PORT}
CMD ["sh", "-c", "serve --port ${PORT}"]EXPOSE
EXPOSE is documentation — it does not publish ports. Publishing happens with -p (host:container) at runtime or ports: in Compose. EXPOSE helps tooling understand the intended ports and enables automatic linking in legacy networks.
# document which port the container listens on
EXPOSE 80
# expose multiple ports
EXPOSE 80 443
# expose with protocol
EXPOSE 53/udp
# EXPOSE does NOT publish the port
# you still need -p at runtime
# docker run -p 8080:80 myapp
# in compose, expose vs ports
# expose: only to linked services
# ports: published to the host
# view exposed ports
docker inspect --format '{{json .Config.ExposedPorts}}' myappLABEL & Metadata
LABEL attaches key-value metadata to images. Adopt the OCI org.opencontainers.image.* labels for interoperability with registries and tools. Labels are useful for tracking version, source, licensing, and CI build info.
# add a label (key=value metadata)
LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="My web app"
# multiple labels in one instruction
LABEL org.opencontainers.image.title="myapp" \
org.opencontainers.image.version="1.0" \
org.opencontainers.image.source="https://github.com/me/myapp"
# view labels
docker inspect --format '{{json .Config.Labels}}' myapp
# filter images by label
docker images --filter "label=version=1.0"
# deprecated: MAINTAINER (use LABEL maintainer instead)
# MAINTAINER Alice <[email protected]>VOLUME in Dockerfile
VOLUME declares a mount point; Docker creates an anonymous volume on first run unless you bind a host path. It signals that the directory holds stateful data. A pitfall: declaring VOLUME after COPY can shadow your copied files with an empty volume.
# declare an anonymous volume mount point
VOLUME /data
# multiple mount points
VOLUME /data /config /logs
# at runtime, Docker creates an anonymous volume
# docker run -v mydata:/data myapp (named instead)
# VOLUME in Dockerfile: any files copied to /data
# are copied into the new anonymous volume on first run
# inspect volumes
docker inspect --format '{{json .Mounts}}' myapp
# avoid: VOLUME after COPY can hide your data
COPY ./config /config
VOLUME /config # new empty volume shadows /configMulti-stage Builds
Basic Multi-stage
Multi-stage builds copy only the final artifacts into the runtime image — build tools, source, and dev dependencies stay in the builder stage. The result: a tiny, secure runtime image. Name stages with AS to reference them in COPY --from.
# stage 1: build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# stage 2: runtime (small)
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Selecting a Build Stage
--target builds only up to the named stage, skipping later stages. This enables a single Dockerfile for dev, test, and production: target 'test' for CI, 'final' for production. BuildKit parallelizes independent stages automatically.
# build only up to a named stage
# docker build --target builder -t myapp:dev .
FROM node:20 AS builder
RUN npm ci && npm run build
FROM node:20-alpine AS test
COPY --from=builder /app /app
RUN npm test
FROM nginx:alpine AS final
COPY --from=builder /app/dist /usr/share/nginx/html
# target a specific stage
# docker build --target test .Slim Runtime Images
For static binaries (Go, Rust), scratch produces the smallest possible image with zero attack surface. Add CA certificates for HTTPS. Distroless offers a minimal base with root CAs and a non-root user, ideal for dynamic languages or when you need a shell-free runtime.
# build stage
FROM golang:1.21 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app -ldflags="-s -w" .
# scratch = empty image, only the static binary
FROM scratch
COPY --from=builder /src/app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]
# result: ~10MB image
# or use distroless for a minimal OS base
FROM gcr.io/distroless/static
COPY --from=builder /src/app /app
ENTRYPOINT ["/app"]Builder Pattern for Compiled Languages
Compile in a full SDK image, then copy the artifact into a runtime-only image (JRE). This dramatically shrinks the image (JDK ~600MB vs JRE ~200MB) and removes build tools from production. Pre-fetching dependencies before COPY improves layer caching.
# stage 1: compile
FROM maven:3.9 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# stage 2: runtime with only the JRE
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /build/target/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# benefits: no JDK, no source, no Maven cache in runtimeCopying from External Images
COPY --from can pull from any image — useful for assembling tools from multiple bases without a build stage. Stages can also be referenced by index (0-based), but naming with AS is clearer. Pin external images by digest for reproducibility.
# copy a binary from another image (no build stage needed)
FROM alpine:3.18
COPY --from=nginx:alpine /usr/sbin/nginx /usr/sbin/nginx
COPY --from=nginx:alpine /etc/nginx /etc/nginx
RUN apk add --no-cache libpcre2
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# use a specific digest for reproducibility
COPY --from=redis:7@sha256:abc... /usr/local/bin/redis-server /usr/local/bin/
# copy from a built stage referenced by index
COPY --from=0 /build/output /appBuildKit Features
BuildKit unlocks cache mounts (npm/pip caches persist between builds without bloating the image), secret mounts (credentials never touch a layer), SSH forwarding for private repos, and heredoc syntax for cleaner multi-line RUN. Add the # syntax directive to pin the Dockerfile frontend version.
# enable BuildKit
# DOCKER_BUILDKIT=1 docker build .
# syntax directive for latest frontend
# syntax=docker/dockerfile:1
FROM alpine
# mount a cache (persists across builds, not in image)
RUN --mount=type=cache,target=/root/.npm npm ci
# mount a secret (never written to layers)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# mount an SSH socket for private repos
RUN --mount=type=ssh git clone [email protected]:me/repo.git
# heredoc for multi-line scripts
RUN <<EOF
set -ex
apk add --no-cache curl
mkdir -p /app/data
EOFVolumes
Creating & Using Volumes
Named volumes are Docker-managed and the recommended way to persist data. They survive container removal and are independent of the host filesystem layout. The :ro suffix mounts read-only. inspect reveals the on-disk path under /var/lib/docker/volumes.
# create a named volume
docker volume create mydata
# use it in a container
docker run -d -v mydata:/data alpine sh -c "sleep 1h"
# inspect the volume
docker volume inspect mydata
# mount read-only
docker run -v mydata:/data:ro alpine ls /data
# mount with a different filesystem (e.g. tmpfs)
docker run --tmpfs /cache alpine
# default volume driver: local
docker volume create --driver local mydataListing & Inspecting Volumes
ls lists named volumes; inspect shows the driver and mountpoint. To find which containers use a volume, filter ps by volume. Volumes live under /var/lib/docker/volumes/<name>/_data on Linux hosts — access via docker exec, not direct host edits.
# list all volumes
docker volume ls
# filter by name
docker volume ls -q --filter name=mydata
# show volume details (driver, mountpoint, options)
docker volume inspect mydata
# find the on-disk path
docker volume inspect --format '{{.Mountpoint}}' mydata
# find containers using a volume
docker ps -a --filter volume=mydata --format '{{.Names}}'
# check volume disk usage
docker system df -v | grep mydataVolume Drivers
The local driver supports NFS, block devices, and tmpfs via mount options. For cloud storage (EBS, Azure Disk), use a plugin driver. Drivers abstract storage so containers move between hosts without changing the run command — key for Swarm/Kubernetes.
# local driver (default, on host disk)
docker volume create --driver local mydata
# NFS volume
docker volume create --driver local \
--opt type=nfs \
--opt o=addr=10.0.0.5,rw \
--opt device=:/export/data nfsvol
# tmpfs (in-memory, ephemeral)
docker run --tmpfs /cache alpine
# block device
docker volume create --driver local \
--opt type=ext4 \
--opt device=/dev/sdb1 blockvol
# third-party drivers (e.g. cloud)
docker volume create --driver rexray/ebs ebsvolBacking Up Volumes
Back up a volume by mounting both the volume and a host directory in a temporary container, then tar the contents. Restore reverses the process. This pattern works for any volume driver and is the basis of automated backup scripts.