← All stacks
DK

Docker

11 sections · 110 entries

Images

Pull an Image

syntax
docker pull <image>[:<tag>]
example
docker pull node:20-alpine
docker pull postgres:16
output
20-alpine: Pulling from library/node
Digest: sha256:a1b2c3...
Status: Downloaded newer image for node:20-alpine

Note If you omit the tag, Docker defaults to :latest, which is a moving target. Always pin a specific version for production work.

Build an Image

syntax
docker build [options] <path>
example
docker build -t myapp:1.0 .
docker build -t myapp:1.0 -f deploy/Dockerfile .
output
=> [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.

Tag an Image

syntax
docker tag <source_image>[:<tag>] <target_image>[:<tag>]
example
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker tag myapp:1.0 myapp:latest

Note Tagging does not duplicate image layers — it just creates a new reference pointing to the same image ID. You can have many tags for one image.

Push an Image to a Registry

syntax
docker push <image>[:<tag>]
example
docker push registry.example.com/myapp:1.0
docker push myuser/api-server:2.4
output
The push refers to repository [registry.example.com/myapp]
1.0: digest: sha256:abc123... size: 1572

Note You must be logged in first (docker login). The image name must include the registry prefix unless you are pushing to Docker Hub.

List Local Images

syntax
docker images [options]
docker image ls [options]
example
docker images
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}'
output
REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
myapp        1.0         a1b2c3d4e5f6   2 hours ago    185MB
node         20-alpine   f6e5d4c3b2a1   3 days ago     130MB

Note Add --filter dangling=true to see images with no tag (often leftovers from rebuilds). These are safe to remove.

Remove an Image

syntax
docker rmi <image> [<image>...]
docker image rm <image>
example
docker rmi myapp:1.0
docker rmi $(docker images -q --filter dangling=true)
output
Untagged: myapp:1.0
Deleted: sha256:a1b2c3...

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.

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.

Inspect Image Details

syntax
docker image inspect <image>
example
docker image inspect node:20-alpine
docker image inspect myapp:1.0 --format '{{.Config.ExposedPorts}}'
output
[
  {
    "Id": "sha256:a1b2c3...",
    "Config": { "Env": [...], "Cmd": [...] }
  }
]

Note The output is JSON. Use --format with Go templates to extract specific fields like environment variables, labels, or exposed ports.

View Image Layer History

syntax
docker history <image>
example
docker history myapp:1.0
docker 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.

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

Containers

Run a Container

syntax
docker run [options] <image> [command]
example
docker run -d --name web-api -p 3000:3000 myapp:1.0
docker 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.

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.

Stop a Running Container

syntax
docker stop [options] <container> [<container>...]
example
docker stop web-api
docker stop -t 5 web-api
output
web-api

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.

Restart a Container

syntax
docker restart <container>
example
docker restart web-api
docker restart -t 3 web-api
output
web-api

Note This performs a stop followed by a start. The container keeps its filesystem and configuration — only the process is restarted.

Remove a Container

syntax
docker rm <container> [<container>...]
docker rm -f <container>
example
docker rm web-api
docker rm -f $(docker ps -aq)
output
web-api

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.

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.

View Container Logs

syntax
docker logs [options] <container>
example
docker logs web-api
docker logs -f --tail 50 web-api
docker logs --since 10m web-api
output
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.

Execute a Command in a Running Container

syntax
docker exec [options] <container> <command>
example
docker exec -it web-api sh
docker exec web-api cat /app/config.json
docker exec -u root web-api apt-get update
output
/ #

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).

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.

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.

Live Resource Usage

syntax
docker stats [container...]
example
docker stats
docker stats web-api db-postgres --no-stream
output
CONTAINER   CPU %   MEM USAGE / LIMIT     NET I/O         BLOCK I/O
web-api     0.50%   85.2MiB / 512MiB      1.2kB / 648B    0B / 4.1kB

Note Without arguments, it shows all running containers. Add --no-stream to get a single snapshot instead of a live-updating display.

Wait for Container to Stop

syntax
docker wait <container>
example
docker wait batch-job && echo 'Job finished'
output
0

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.

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.

Run Options

Port Mapping (-p)

syntax
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.0
docker 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).

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.0
docker run -v pgdata:/var/lib/postgresql/data postgres:16
docker 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.

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.0
docker 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.

Detached Mode (-d)

syntax
docker run -d <image>
example
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.

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.

Specify Network (--network)

syntax
docker run --network <network_name> <image>
example
docker run -d --name web-api --network app-net myapp:1.0
docker 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.

Restart Policy (--restart)

syntax
docker run --restart <policy> <image>
example
docker run -d --restart unless-stopped --name web-api myapp:1.0
docker 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.

Memory Limit (--memory)

syntax
docker run --memory <limit> <image>
example
docker run -d --memory 512m --name web-api myapp:1.0
docker 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.

CPU Limit (--cpus)

syntax
docker run --cpus <number> <image>
example
docker run -d --cpus 1.5 --name worker myapp:1.0
docker 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.

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.

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.

Dockerfile

FROM — Set Base Image

syntax
FROM <image>[:<tag>] [AS <stage_name>]
example
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.

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.

COPY — Copy Files into Image

syntax
COPY [--chown=<user>:<group>] <src>... <dest>
example
COPY package.json package-lock.json ./
COPY --chown=appuser:appuser . /app/

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.

ADD — Copy with Extras

syntax
ADD <src> <dest>
example
ADD app.tar.gz /app/
ADD https://example.com/config.json /app/config.json

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.

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.

CMD — Default Container Command

syntax
CMD ["executable", "arg1", "arg2"]
CMD command arg1 arg2
example
CMD ["node", "server.js"]
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

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.

ENTRYPOINT — Fixed Container Executable

syntax
ENTRYPOINT ["executable", "arg1"]
ENTRYPOINT command arg1
example
ENTRYPOINT ["python", "manage.py"]
CMD ["runserver", "0.0.0.0:8000"]

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.

ENV — Set Environment Variables

syntax
ENV <key>=<value> [<key>=<value>...]
example
ENV NODE_ENV=production
ENV APP_PORT=3000 LOG_LEVEL=info

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.

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.

EXPOSE — Document Container Port

syntax
EXPOSE <port>[/<protocol>]
example
EXPOSE 3000
EXPOSE 8443/tcp

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.

VOLUME — Declare Mount Point

syntax
VOLUME ["/path/to/dir"]
example
VOLUME ["/var/lib/postgresql/data"]

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.

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.

LABEL — Add Metadata

syntax
LABEL <key>=<value> [<key>=<value>...]
example
LABEL maintainer="team@example.com"
LABEL version="2.1" description="API backend service"

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.

HEALTHCHECK — Container Health Probe

syntax
HEALTHCHECK [options] CMD <command>
HEALTHCHECK NONE
example
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

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.

Multi-Stage Builds

syntax
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).

.dockerignore — Exclude Build Context

syntax
# .dockerignore file
pattern
dir/
example
node_modules
.git
*.md
.env
dist
.DS_Store
coverage

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.

Docker Compose

Define Services

syntax
# compose.yaml
services:
  <service_name>:
    image: <image>
    build: <context>
example
services:
  api:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass

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.

Volumes in Compose

syntax
services:
  db:
    volumes:
      - <name>:<container_path>
volumes:
  <name>:
example
services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
  pgdata:

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.

Networks in Compose

syntax
services:
  api:
    networks:
      - frontend
      - backend
networks:
  frontend:
  backend:
example
services:
  web:
    image: nginx:alpine
    networks:
      - frontend
  api:
    build: .
    networks:
      - frontend
      - backend
  db:
    image: postgres:16
    networks:
      - backend
networks:
  frontend:
  backend:

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.

Port Mapping in Compose

syntax
services:
  api:
    ports:
      - "<host>:<container>"
example
services:
  api:
    ports:
      - "8080:3000"
      - "127.0.0.1:9229:9229"

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.

Environment Variables in Compose

syntax
services:
  api:
    environment:
      - KEY=value
    env_file:
      - .env
example
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.

Service Dependencies

syntax
services:
  api:
    depends_on:
      <service>:
        condition: <condition>
example
services:
  api:
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

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.

Build Configuration

syntax
services:
  api:
    build:
      context: <path>
      dockerfile: <file>
      args:
        KEY: value
example
services:
  api:
    build:
      context: .
      dockerfile: docker/Dockerfile.prod
      args:
        NODE_VERSION: 20
    image: myapp:latest

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.

Profiles

syntax
services:
  debug-tools:
    profiles:
      - debug
example
services:
  api:
    build: .
  db:
    image: postgres:16
  pgadmin:
    image: dpage/pgadmin4
    profiles:
      - debug
    ports:
      - "5050:80"
# Run with: docker compose --profile debug up

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.

Start & Stop the Stack

syntax
docker compose up [options]
docker compose down [options]
example
docker compose up -d
docker compose up -d --build
docker 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.

Logs, Exec & Status

syntax
docker compose logs [service]
docker compose exec <service> <command>
docker compose ps
example
docker compose logs -f api
docker compose exec db psql -U postgres
docker compose ps
output
NAME              STATUS          PORTS
myproject-api-1   Up 5 minutes    0.0.0.0:3000->3000/tcp
myproject-db-1    Up 5 minutes    5432/tcp

Note logs -f follows all services by default — pass a service name to filter. exec opens a shell or runs a command inside a running service container.

Compose Watch (Hot Reload)

syntax
docker compose watch
# or in compose.yaml:
services:
  api:
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
example
services:
  api:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json
# Then: docker compose watch

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.

Override Files

syntax
# compose.yaml — base config
# compose.override.yaml — auto-loaded overrides
# docker compose -f compose.yaml -f compose.prod.yaml up
example
# compose.yaml
services:
  api:
    build: .
    ports:
      - "3000:3000"

# compose.override.yaml (auto-loaded in dev)
services:
  api:
    volumes:
      - ./src:/app/src
    environment:
      DEBUG: "true"

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.

Volumes & Storage

Named Volumes

syntax
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.

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.0
docker 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.

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.0
docker 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.

Inspect a Volume

syntax
docker volume inspect <name>
example
docker volume inspect pgdata
output
[
  {
    "Name": "pgdata",
    "Driver": "local",
    "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
    "CreatedAt": "2025-11-15T10:30:00Z"
  }
]

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.

List & Remove Volumes

syntax
docker volume ls
docker volume rm <name>
docker volume prune
example
docker volume ls
docker volume rm pgdata
docker volume prune
output
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.

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.

Anonymous Volumes

syntax
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.

Networking

Bridge Network (Default)

syntax
docker run <image>
# Container joins the default bridge network
example
docker run -d --name web nginx:alpine
docker 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.

Host Network

syntax
docker run --network host <image>
example
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.

No Network

syntax
docker run --network none <image>
example
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.

Create a Custom Network

syntax
docker network create [options] <name>
example
docker network create app-net
docker network create --subnet 172.20.0.0/16 app-net
output
a1b2c3d4e5f6...

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.

Connect & Disconnect Containers

syntax
docker network connect <network> <container>
docker network disconnect <network> <container>
example
docker network connect app-net web-api
docker network disconnect bridge web-api

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.

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:16
docker 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.

List & Inspect Networks

syntax
docker network ls
docker network inspect <name>
example
docker network ls
docker network inspect app-net --format '{{range .Containers}}{{.Name}} {{end}}'
output
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.

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.

Registry & Distribution

Log In to a Registry

syntax
docker login [registry]
docker login -u <username> <registry>
example
docker login
docker login ghcr.io
docker login registry.example.com -u deploy-bot
output
Login Succeeded

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.

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.0
docker tag myapp:1.0 ghcr.io/myorg/myapp:latest
docker push ghcr.io/myorg/myapp:1.0
docker 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.

Tag Naming Conventions

syntax
<image>:<version>
<image>:<version>-<variant>
example
myapp:2.1.0
myapp:2.1.0-alpine
myapp:2.1
myapp:latest
myapp:main-abc1234

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.

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:2
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0
docker 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.

Multi-Platform Builds with Buildx

syntax
docker buildx build --platform <platforms> -t <tag> [--push] .
example
docker buildx create --name multiarch --use
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/myorg/myapp:1.0 \
  --push .

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.

Inspect Remote Image Manifests

syntax
docker manifest inspect <image>
example
docker manifest inspect node:20-alpine
docker buildx imagetools inspect ghcr.io/myorg/myapp:1.0

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.

Debugging

Follow Logs in Real Time

syntax
docker logs -f [--tail <n>] <container>
example
docker logs -f --tail 100 web-api
docker logs -f --since 5m web-api
output
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.

Open a Debug Shell

syntax
docker exec -it <container> <shell>
example
docker exec -it web-api sh
docker exec -it db-postgres bash
docker exec -it web-api sh -c 'ls -la /app && cat /app/.env'
output
/app #

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.

Inspect Container Details

syntax
docker inspect <container>
example
docker inspect web-api
docker inspect web-api --format '{{.NetworkSettings.IPAddress}}'
docker inspect web-api --format '{{json .State}}'
output
172.17.0.3

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.

View Processes Inside a Container

syntax
docker top <container>
example
docker top web-api
output
UID     PID     PPID    CMD
root    12345   12300   node server.js
root    12350   12345   /usr/bin/node worker.js

Note Shows the process tree from the host's perspective. Useful for confirming which processes are actually running and spotting zombie or unexpected processes.

Monitor Docker Events

syntax
docker events [options]
example
docker events
docker events --filter container=web-api --filter event=die
docker events --since 10m --until 5m
output
2025-11-15T10:30:00.000 container start abc123 (name=web-api)
2025-11-15T10:30:05.000 container die abc123 (exitCode=137, name=web-api)

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).

Disk Usage Breakdown

syntax
docker system df [-v]
example
docker system df
docker system df -v
output
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.

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.

Copy Files To/From a Container

syntax
docker cp <container>:<path> <host_path>
docker cp <host_path> <container>:<path>
example
docker cp web-api:/app/logs/error.log ./error.log
docker cp ./hotfix.js web-api:/app/hotfix.js

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.

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.

Best Practices

Optimize Layer Caching

syntax
# 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.

Use Small Base Images

syntax
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.

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.

Always Use .dockerignore

syntax
# .dockerignore
example
node_modules
.git
*.md
.env*
coverage
dist
.DS_Store
__pycache__
*.pyc
.venv

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.

Add Health Checks

syntax
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD <check>
example
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

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.

Pin All Versions

syntax
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.

Use Multi-Stage Builds

syntax
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.

Scan Images for Vulnerabilities

syntax
docker scout cves <image>
docker scout quickview <image>
example
docker scout quickview myapp:1.0
docker scout cves --only-fixed myapp:1.0
output
✗ 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.

Combine RUN Commands

syntax
RUN command1 && command2 && command3
example
# 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.

Handle Signals with a Proper Init

syntax
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
example
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.

Common Mistakes

Running Containers as Root

syntax
# 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.

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.

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.

Dangling Images & Orphan Volumes

syntax
# Check dangling images:
docker images -f dangling=true
# Check orphan volumes:
docker volume ls -f dangling=true
example
# Frequent rebuilds leave untagged images:
docker images -f dangling=true
# REPOSITORY  TAG     SIZE
# <none>      <none>  450MB
# <none>      <none>  450MB

# Clean them:
docker image prune
docker volume prune

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.

Port Conflicts on the Host

syntax
# Error: bind: address already in use
example
# Problem:
docker run -d -p 3000:3000 --name api-v1 myapp:1.0
docker 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.

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.

Using ADD When You Mean COPY

syntax
# ADD has hidden behaviors:
ADD app.tar.gz /app/    # auto-extracts
ADD https://x.com/f /f  # downloads
example
# 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.

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.0
docker 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.