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.
# back up a volume to a tar file
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
tar czf /backup/mydata.tar.gz -C /data .
# restore a volume from a tar file
docker volume create mydata2
docker run --rm -v mydata2:/data -v $(pwd):/backup alpine \
tar xzf /backup/mydata.tar.gz -C /data
# migrate data between volumes
docker run --rm -v src:/src -v dst:/dst alpine \
cp -a /src/. /dst/
# snapshot using a temporary container
docker run --rm -v mydata:/data alpine ls -la /dataRemoving Volumes
A volume cannot be removed while a container references it (remove the container first). prune removes only unused volumes. Volume deletion is irreversible — the data is permanently lost. Always back up production volumes before pruning.
# remove a named volume (must not be in use)
docker volume rm mydata
# remove all unused volumes
docker volume prune
# remove volumes older than 24h not used
docker volume prune --filter "until=24h"
# force remove (even if a stopped container referenced it)
docker volume rm -f mydata
# remove with a label filter
docker volume prune --filter "label=temporary"
# CAUTION: data is gone permanently
docker volume rm mydataSharing Volumes Between Containers
Multiple containers can mount the same volume for sharing state. --volumes-from clones another container's volume mounts — handy for sidecar patterns. Add :ro for read-only consumers. Coordinate concurrent writes to avoid corruption; some apps need a lock.
# share a volume between two containers
docker run -d --name db -v shared:/data alpine sleep 1h
docker run -d --name app -v shared:/data alpine sleep 1h
# use --volumes-from to copy a container's mounts
docker run -d --name helper --volumes-from db alpine sleep 1h
# read-only shared volume
docker run -d --name reader -v shared:/data:ro alpine sleep 1h
# compose: shared volume across services
# volumes:
# shared:
# services share 'shared' via volumes: ['shared:/data']
# verify both see the same data
docker exec db sh -c "echo hi > /data/msg"
docker exec app cat /data/msgBind Mounts
Basic Bind Mount
Bind mounts map a host path directly into the container — ideal for live development where host edits reflect instantly. Use absolute host paths. The :ro suffix prevents the container from writing back to the host. Modern syntax uses --mount for clarity.
# bind mount a host directory (modern syntax)
docker run -d --type bind --source $(pwd)/app --target /app nginx
# short form (-v)
docker run -d -v $(pwd)/app:/app nginx
# absolute host path required
docker run -v /home/me/app:/app nginx
# mount a single file
docker run -v /home/me/app.conf:/etc/app/app.conf:ro nginx
# mount read-only
docker run -v $(pwd)/app:/app:ro nginx
# compose equivalent
# volumes:
# - ./app:/appRead-only Bind Mount
Read-only mounts protect host configuration from container writes — critical for security. --read-only makes the entire root filesystem immutable, requiring explicit writable mounts for /tmp and log dirs. This hardens containers against compromise.
# mount config read-only
docker run -v /etc/myapp:/etc/myapp:ro myapp
# --mount equivalent with explicit type
docker run --mount type=bind,source=/etc/myapp,target=/etc/myapp,readonly myapp
# read-only root filesystem (extra security)
docker run --read-only -v /tmp myapp
# combine read-only root with a writable tmpfs
docker run --read-only --tmpfs /tmp --tmpfs /var/cache myapp
# verify the mount is read-only
docker exec myapp touch /etc/myapp/test # should failDevelopment Workflow
Mount source code so host edits reflect instantly in the container. The 'anonymous volume for node_modules' trick prevents the host's empty node_modules from shadowing the installed deps in the image. Polling may be needed on Windows/Mac for file-watch reliability.
# live-reload: mount source, run dev server
docker run -d -p 3000:3000 -v $(pwd)/src:/app/src node:20 \
sh -c "cd /app && npm run dev"
# mount node_modules separately (avoid host override)
docker run -d -v $(pwd):/app -v /app/node_modules node:20 npm start
# compose dev workflow
# services:
# web:
# volumes:
# - ./src:/app/src
# - /app/node_modules # anonymous, isolates deps
# hot reload with file watching (polling for Windows/Mac)
docker run -e CHOKIDAR_USEPOLLING=true -v $(pwd):/app node:20 npm startSELinux & AppArmor Labels
On SELinux-enabled hosts (RHEL/CentOS/Fedora), bind mounts need a :z or :Z suffix to relabel the host path so the container can access it. :z shares the path among containers; :Z makes it private. Forgetting the suffix causes 'permission denied' errors.
# SELinux: :z (shared) or :Z (private) suffix
docker run -v /host/path:/container/path:z myapp
docker run -v /host/path:/container/path:Z myapp
# :z labels the host path as shared among containers
# :Z labels it private to this container
# disable SELinux isolation (NOT recommended)
docker run --security-opt label=disable myapp
# AppArmor profile
docker run --security-opt apparmor=docker-default myapp
# custom AppArmor profile
docker run --security-opt apparmor=myprofile myappMount vs Volume vs tmpfs
Volumes: Docker-managed, portable, best for persistent data. Bind mounts: host paths, best for dev and config injection. tmpfs: in-memory, best for secrets and ephemeral caches. --mount syntax is more explicit and self-documenting than -v.
# named volume (Docker-managed, portable)
docker run -v mydata:/data nginx
# bind mount (host path, host-dependent)
docker run -v /home/me/data:/data nginx
# --mount explicit syntax (recommended)
docker run --mount type=volume,source=mydata,target=/data nginx
docker run --mount type=bind,source=/home/me/data,target=/data nginx
docker run --mount type=tmpfs,target=/cache nginx
# tmpfs (in-memory, ephemeral, fast)
docker run --tmpfs /cache nginx
# tmpfs with size limit
docker run --mount type=tmpfs,target=/cache,tmpfs-size=64m nginxBind Mounts in Compose
In Compose, a relative path (./html) creates a bind mount, while a name (logs) referencing the volumes: section creates a named volume. A bare path (/cache) creates an anonymous volume. The long-form syntax makes options like read_only explicit.
# docker-compose.yml
services:
web:
image: nginx
volumes:
# relative host path = bind mount
- ./html:/usr/share/nginx/html:ro
# named volume
- logs:/var/log/nginx
# anonymous volume
- /cache
volumes:
logs: # declare named volumes here
# use ~ (tilde) — Compose expands it
volumes:
- ~/config:/etc/app:ro
# long-form syntax with options
volumes:
- type: bind
source: ./html
target: /usr/share/nginx/html
read_only: trueNetworking
Bridge Network (Default)
The default bridge network lacks automatic DNS — containers must use IPs. Custom bridge networks provide DNS-based service discovery: containers resolve each other by name. This is why Compose creates a custom network for each project automatically.
# the default bridge network (named 'bridge')
docker run -d --name web nginx
# create a custom bridge network (recommended)
docker network create mynet
# attach a container to a custom network
docker run -d --name web --network mynet nginx
docker run -d --name db --network mynet postgres
# containers on the same custom network can resolve by name
docker exec web ping db # DNS resolves 'db'
# the default bridge does NOT support DNS names
# always use a custom bridge for multi-container appsHost Network
Host networking removes the network namespace — the container shares the host's interfaces and ports. It offers the best performance but sacrifices isolation and port mapping. Available only on Linux; on Mac/Windows, it maps to a VM network, not the host.
# use the host's network stack directly (no isolation)
docker run --network host nginx
# host network disables port mapping
# -p is ignored when --network host is used
# useful for performance-sensitive apps
docker run --network host -d myapp
# only works on Linux (not Docker Desktop on Mac/Windows)
# inspect host network
docker network inspect host
# bound to host interfaces — be careful with privileged ports
docker run --network host --cap-add NET_ADMIN myappOverlay Network (Swarm)
Overlay networks span multiple Swarm nodes, enabling secure cross-host communication with built-in DNS. --attachable lets standalone containers join an overlay (useful for debugging). Encryption secures traffic between nodes; enable it for sensitive workloads.
# create an overlay network (requires Swarm mode)
docker network create -d overlay myoverlay
# overlay with encryption
docker network create -d overlay -o encrypted myoverlay
# deploy a service on the overlay
docker service create --network myoverlay --name web nginx
# multi-host communication for Swarm services
# containers on different nodes resolve by service name
# node-local network bridged to overlay
docker network create -d overlay --attachable myoverlay
docker run -d --network myoverlay alpine # standalone containerCustom Networks
Custom networks let you control subnets, gateways, and isolation. --internal prevents outbound internet access. macvlan gives containers a real presence on the physical LAN (each gets its own MAC) — useful for legacy apps but breaks host-to-container communication by default.
# create a bridge network with a subnet
docker network create --subnet 172.20.0.0/16 mynet
# assign a static IP
docker run -d --network mynet --ip 172.20.0.10 nginx
# set a custom gateway
docker network create --subnet 172.20.0.0/16 \
--gateway 172.20.0.1 mynet
# restrict to internal only (no external access)
docker network create --internal mynet
# specify a driver and options
docker network create -d bridge \
-o com.docker.network.bridge.name=br0 mynet
# macvlan (container gets a MAC on the physical network)
docker network create -d macvlan \
--subnet 192.168.1.0/24 -o parent=eth0 mymacvlanContainer DNS
Docker's embedded DNS (127.0.0.11) resolves container names on user-defined networks. The default bridge has no DNS. --dns adds external resolvers; --add-host injects static entries. Use service names (not IPs) for inter-container communication so redeploys don't break DNS.
# containers on custom networks resolve by name
docker network create mynet
docker run -d --name db --network mynet postgres
docker run -it --network mynet alpine nslookup db
# the embedded DNS server is at 127.0.0.11
docker exec web cat /etc/resolv.conf
# add custom DNS servers
docker run --dns 8.8.8.8 alpine nslookup example.com
# add DNS search domains
docker run --dns-search example.com alpine nslookup myhost
# add a host entry (like /etc/hosts)
docker run --add-host myservice:10.0.0.5 alpine ping myservice
# extra hosts in compose
# extra_hosts:
# - "myservice:10.0.0.5"Network Inspection & Management
network connect/disconnect lets you attach a running container to additional networks without restarting — useful for sidecars. inspect shows connected containers, subnet, and driver. prune removes networks not used by any container.
# list all networks
docker network ls
# inspect a network (containers, IPAM, driver)
docker network inspect mynet
# see which containers are on a network
docker network inspect --format '{{range .Containers}}{{.Name}} {{end}}' mynet
# connect a running container to another network
docker network connect mynet2 web
# disconnect a container from a network
docker network disconnect mynet web
# remove an unused network
docker network rm mynet
docker network prunePort Mapping
Basic Port Mapping
-p host:container maps a host port to a container port. If you omit the host port, Docker picks one (use docker port to find it). Each host port can only map to one container — use a reverse proxy or different host ports for multiple instances.
# map host:container
docker run -d -p 8080:80 nginx
# map to a random host port
docker run -d -p 80 nginx
docker port $(docker ps -lq) 80 # find the assigned port
# map multiple ports
docker run -d -p 8080:80 -p 8443:443 nginx
# map the same port on host and container
docker run -d -p 80:80 nginx
# explicit protocol (default tcp)
docker run -d -p 53:53/udp dns
# long form
docker run -d --publish 8080:80 nginxBinding to a Specific IP
Prefixing with an IP controls which interface the port binds to. 127.0.0.1 keeps the service local-only — essential for databases you don't want exposed. 0.0.0.0 (default) binds to all interfaces. Always bind databases to localhost in production.
# bind to a specific host IP
docker run -d -p 127.0.0.1:8080:80 nginx
# bind to localhost only (not exposed externally)
docker run -d -p 127.0.0.1:5432:5432 postgres
# bind to all interfaces (default)
docker run -d -p 0.0.0.0:8080:80 nginx
# bind to an IPv6 address
docker run -d -p [::1]:8080:80 nginx
# multiple bindings
docker run -d -p 127.0.0.1:8080:80 -p 0.0.0.0:8443:443 nginx
# compose: specify IP
# ports:
# - "127.0.0.1:5432:5432"UDP & SCTP Ports
The /udp or /sctp suffix selects the protocol; TCP is the default. For services like DNS that need both, declare both mappings. Port ranges map 1:1 between host and container — useful for WebRTC, RTP, or passive FTP. Ensure firewall rules match the protocols.
# UDP port (DNS, syslog, statsd)
docker run -d -p 53:53/udp coredns
# both TCP and UDP
docker run -d -p 53:53/tcp -p 53:53/udp coredns
# SCTP (e.g. telecom signaling)
docker run -d -p 3868:3868/sctp myapp
# range of ports (host range maps to container range)
docker run -d -p 8000-8100:8000-8100 myapp
# UDP range
docker run -d -p 50000-50010:50000-50010/udp myapp
# compose with protocol
# ports:
# - "53:53/udp"Range of Ports
Port ranges map 1:1 (host 8000-8005 to container 8000-8005). -P (capital) auto-publishes every EXPOSE port to a random high host port — handy for quick tests. Use docker ps to see the actual mappings when ports are random.
# map a range of host ports to container ports
docker run -d -p 8000-8005:8000-8005 myapp
# map host range to a single container port (NOT supported)
# this maps range-to-range only
# random port for each (useful for testing)
docker run -d -p 80 -p 443 nginx
docker port $(docker ps -lq)
# expose a range with EXPOSE (documentation only)
# EXPOSE 8000-8005
# publish all exposed ports at once
docker run -d -P nginx # capital P publishes all EXPOSE ports
# view the random mappings
docker ps --format '{{.Names}} {{.Ports}}'Exposing vs Publishing
EXPOSE documents intent; -p actually publishes. In Compose, expose shares a port only with other services on the same network, while ports publishes to the host. This distinction matters for databases: expose internally, never publish to the host in production.
# Dockerfile: EXPOSE is documentation, NOT a publish
EXPOSE 80 443
# runtime: -p PUBLISHES the port to the host
docker run -d -p 8080:80 nginx
# -P publishes all EXPOSE ports to random host ports
docker run -d -P nginx
# compose: expose vs ports
services:
web:
image: nginx
expose:
- "80" # only to linked services, NOT to host
ports:
- "8080:80" # published to the host
# view exposed (not published) ports
docker inspect --format '{{json .Config.ExposedPorts}}' nginxRandom Port Mapping
Omitting the host port lets Docker pick a random ephemeral port (32768-60999 by default). Use docker port or docker ps to discover it. This is useful for scaling identical services on one host. Adjust the ephemeral range in /proc/sys/net/ipv4/ip_local_port_range on Linux.
# publish to a random high host port
docker run -d -p 80 nginx
# publish all EXPOSE ports randomly
docker run -d -P nginx
# find the assigned port
docker port <container> 80
docker port <container>
# see mappings in ps output
docker ps --format 'table {{.Names}} {{.Ports}}'
# prefer a range for random allocation
docker run -d -p 30000-40000:80 nginx
# compose: random host port
# ports:
# - "80" # random host port, container port 80Environment Variables
Setting Env Vars
-e (or --env) sets environment variables in the container. -e VAR (no value) inherits the variable from the host shell. For sensitive values, prefer --env-file or Docker secrets over plain -e, which is visible in docker inspect and process listings.
# single variable
docker run -d -e MYSQL_ROOT_PASSWORD=secret mysql
# multiple variables
docker run -d \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=mydb \
-e MYSQL_USER=app \
mysql
# pass from the host environment
docker run -d -e HOST_PWD mysql
# HOST_PWD in the container = $HOST_PWD on host
# set a variable without a value (passes host value or empty)
docker run -e DEBUG alpine env
# long form
docker run --env MYSQL_ROOT_PASSWORD=secret mysqlEnv Files
--env-file loads variables from a file, keeping the run command clean and secrets out of shell history. Compose auto-loads a top-level .env for variable substitution in the compose file itself. Comments (#) and blank lines are ignored; quotes are preserved literally.
# .env file
# DB_HOST=db
# DB_USER=app
# DB_PASSWORD=secret
# load variables from a file
docker run --env-file .env myapp
# load multiple env files
docker run --env-file .env --env-file .env.local myapp
# Compose reads .env automatically for variable substitution
# and supports env_file per service:
# services:
# web:
# env_file:
# - .env
# - .env.local
# env file with comments and blank lines
docker run --env-file config.env myappEnv Vars in Dockerfile
ENV bakes variables into the image, visible at build and runtime. ARG is build-time only — convert to ENV if the runtime needs it. Override baked values with -e at runtime. Note: env vars are visible in docker inspect, so don't store secrets there.
# ENV sets a persistent variable
ENV NODE_ENV=production
ENV PATH=/app/bin:$PATH
# use ARG for build-time only, ENV for runtime
ARG BUILD_VERSION=1.0
ENV APP_VERSION=${BUILD_VERSION}
# view all env vars
docker exec web env
# inspect a specific variable
docker exec web printenv NODE_ENV
# override at runtime
docker run -e NODE_ENV=development myapp
# set via Compose
# environment:
# - NODE_ENV=development
# - DEBUG=trueDynamic Env Vars & Substitution
Shell expansion ($(...) or ${VAR}) happens on the host before Docker sees the value. In Compose, ${VAR} is substituted from the .env file. In exec-form CMD, ${VAR} is NOT expanded — wrap with sh -c. Pass the host UID for correct file ownership in bind mounts.
# pass a computed value at runtime
docker run -e STARTUP_TIME=$(date +%s) myapp
# use host shell expansion
docker run -e USER_ID=$(id -u) myapp
# Compose variable substitution from .env
# services:
# web:
# environment:
# DB_HOST: ${DB_HOST}
# .env file:
# DB_HOST=db
# Compose with default values
# DB_HOST: ${DB_HOST:-localhost}
# pass the current user for permission matching
docker run -u $(id -u):$(id -g) -e HOME=/tmp myapp
# resolve env at runtime with sh -c (not in exec form)
ENV PORT=3000
CMD ["sh", "-c", "node server.js --port ${PORT}"]Compose Environment
Compose's environment accepts list or map syntax. env_file loads values into the container; .env (top-level) provides variables for ${} substitution in the compose file itself. Use the map form for readability with many variables.
# docker-compose.yml
services:
web:
image: myapp
environment:
- NODE_ENV=production
- DEBUG=false
- DB_HOST=db
# map form (cleaner for many vars)
environment:
NODE_ENV: production
DB_HOST: db
db:
image: postgres
env_file:
- .env.db
environment:
POSTGRES_DB: ${DB_NAME} # from .env
# top-level .env for substitution
# DB_NAME=mydbSecrets Management
Secrets are mounted as files under /run/secrets, never as env vars, and are encrypted in transit and at rest in Swarm. Avoid baking secrets into images (ENV) or passing via -e — both leak via docker inspect and image layers. BuildKit secret mounts keep build-time credentials out of layers.
# Docker Swarm secrets (encrypted, mounted as files)
echo "supersecret" | docker secret create db_password -
docker service create --secret db_password myapp
# read in the app
# cat /run/secrets/db_password
# Compose secrets (v3.1+)
# services:
# web:
# secrets:
# - db_password
# secrets:
# db_password:
# file: ./secrets/db_password.txt
# BuildKit secret mount (build-time, not in layers)
# RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# avoid: baking secrets in ENV or -e (visible in inspect)
docker run -e API_KEY=xxx myapp # NOT recommendedDocker Compose Basics
Basic docker-compose.yml
Compose defines a multi-container app in a single YAML file. services describes each container; volumes and networks are declared at the top level. depends_on controls startup order. docker compose up -d starts everything in the background.
# docker-compose.yml (Compose spec v2)
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
# run: docker compose up -dCompose Commands
up creates and starts, down stops and removes. Add -v to down to delete volumes (data loss). exec runs a command in a running service; run starts a new container for a one-off task. --scale launches multiple replicas of a service on the same host.
# start all services (foreground)
docker compose up
# start in background (detached)
docker compose up -d
# rebuild images before starting
docker compose up -d --build
# stop and remove containers (keeps volumes)
docker compose down
# also remove volumes (data loss)
docker compose down -v
# view logs (all services)
docker compose logs -f
# scale a service
docker compose up -d --scale worker=3
# run a one-off command in a service
docker compose exec web sh
docker compose run --rm web python manage.py migrateService Dependencies
depends_on controls startup order, but the dependency may not be 'ready' — use condition: service_healthy with a healthcheck to wait until the database accepts connections. service_completed_successfully waits for init containers. Apps should still retry connections.
services:
web:
image: myapp
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: on-failure
db:
image: postgres
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
retries: 5
redis:
image: redis
# conditions: service_started | service_healthy | service_completed_successfullyBuild in Compose
build defines how to build an image from source; image tags the result. context is sent to the builder (keep it small). target selects a multi-stage build stage. --build forces a rebuild on up. Build args pass values at build time.
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
args:
VERSION: 1.0
target: final
image: myapp:1.0
ports:
- "8080:80"
worker:
build: ./worker # shorthand: context only
image: myworker:1.0
# build and tag
docker compose build
docker compose build --no-cache web
docker compose up -d --buildVolumes in Compose
A relative path (./html) is a bind mount; a name (logs) declared under top-level volumes: is a named volume; a bare path (/cache) is anonymous. Named volumes can specify a driver and options (e.g. NFS). Compose prefixes volume names with the project name.
services:
web:
image: nginx
volumes:
# named volume
- logs:/var/log/nginx
# bind mount (relative path)
- ./html:/usr/share/nginx/html:ro
# anonymous volume
- /cache
db:
image: postgres
volumes:
- dbdata:/var/lib/lib/postgresql/data
volumes:
logs:
dbdata:
driver: local
driver_opts:
type: nfs
device: ":/export/data"
o: addr=10.0.0.5,rwNetworks in Compose
Compose creates a default network for all services, but defining custom networks lets you isolate tiers. web bridges frontend and backend. internal: true blocks internet access for the backend. Services on the same network resolve each other by service name via DNS.