Cheatsheet Docker
Plataforma de contentores para criar, distribuir e executar aplicações
Docker
Images
List Images
docker images
docker image ls -a
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
docker images --filter "dangling=true"
docker images --filter "reference=nginx*"docker images shows local images. --format customizes columns with Go templates. dangling=true filters untagged images (build intermediates). reference filters by name.
Remove Images
# Remove by name:tag: docker rmi nginx:latest # Remove by ID: docker rmi a1b2c3d4e5f6 # Force (even with stopped containers): docker rmi -f my-app:old # Remove all untagged (dangling): docker image prune # Remove ALL unused ones: docker image prune -a
docker rmi removes images. -f forces even with associated containers. docker image prune cleans dangling images. prune -a removes all without active containers — frees a lot of space in CI/CD.
Search and Docker Hub
# Search images on Docker Hub: docker search nginx docker search --filter "is-official=true" python docker search --filter "stars=100" redis # Results: NAME, DESCRIPTION, STARS, OFFICIAL # Official (curated) images: docker pull nginx # official (no prefix) docker pull bitnami/nginx # community (with prefix)
docker search searches Docker Hub via CLI. is-official=true filters images curated by Docker. Official images have no prefix. Community uses user/image. Always prefer official or verified ones.
Pull an Image
docker pull nginx:latest docker pull node:20-alpine docker pull postgres:16 # Specific tag: docker pull python:3.12-slim # All tags (careful, heavy): docker pull -a ubuntu # Verify: docker images nginx
docker pull downloads an image from the registry. Always use specific tags (node:20-alpine) instead of latest. alpine and slim tags are lighter. -a downloads all tags.
Inspect and History
# Full details (JSON):
docker inspect nginx:latest
# Specific fields:
docker inspect --format="{{.Config.ExposedPorts}}" nginx
# Layer history:
docker history nginx:latest
docker history --no-trunc nginx # full commands
# Size per layer:
docker history --format "{{.Size}}\t{{.CreatedBy}}" nginxdocker inspect shows the full configuration (env, ports, volumes). docker history lists layers and the commands that created them. --no-trunc shows entire commands. Useful for debugging and size optimization.
Multi-arch Images (ARM/AMD)
# See supported platforms: docker manifest inspect nginx:latest # Pull for a specific architecture: docker pull --platform linux/arm64 nginx:latest docker pull --platform linux/amd64 node:20 # Multi-arch build (with buildx): docker buildx create --use docker buildx build --platform linux/amd64,linux/arm64 -t user/app:1.0 --push .
multi-arch images support AMD64 and ARM64 under the same tag. --platform forces an architecture. docker buildx builds images for multiple platforms. Essential for Apple Silicon (ARM) and cloud servers (AMD64).
Build an Image
# Build from the Dockerfile in the current directory: docker build -t my-app:1.0 . # With a specific file: docker build -f Dockerfile.prod -t my-app:prod . # With build args: docker build --build-arg NODE_ENV=production -t app . # Without cache: docker build --no-cache -t app . # Verify: docker images my-app
docker build -t creates an image with name and tag. The . is the build context (accessible files). -f specifies an alternative Dockerfile. --no-cache forces a full rebuild. --build-arg passes variables.
Save and Load (Transfer)
# Export an image to a tar file: docker save -o nginx-backup.tar nginx:latest # Compress: docker save nginx:latest | gzip > nginx.tar.gz # Import on another server: docker load -i nginx-backup.tar gunzip -c nginx.tar.gz | docker load # Verify: docker images nginx
docker save exports an image the .tar (with all layers). docker load imports it. Ideal for transferring between servers without a registry. Compress with gzip to reduce size.
Tag and Rename
# Create a new tag for an existing image: docker tag my-app:1.0 my-app:latest docker tag my-app:1.0 registry.io/user/my-app:1.0 # Format: docker tag SOURCE TARGET # The tag is just a pointer — it does not duplicate data docker images # same IMAGE ID, different tags
docker tag creates an alias for the same image (no duplication). Required before push to registries: the name must include the registry (registry.io/user/app). Multiple tags can point to the same ID.
Import and Export (Container)
# Export a container filesystem: docker export -o app-fs.tar my-container # Import the a new image: docker import app-fs.tar my-app:imported # Difference vs save/load: # save → full image (layers + metadata) # export → filesystem only (no history) # import → flat image (1 layer)
docker export extracts a container filesystem (no layers). docker import creates a flat image. History and metadata are lost. Use save/load for images; export/import for quick snapshots.
Containers
Create and Run (run)
# Basic: docker run nginx # Background + name + port: docker run -d --name web -p 8080:80 nginx # Interactive (shell): docker run -it ubuntu:22.04 /bin/bash # With environment variables: docker run -d -e POSTGRES_PASSWORD=secret postgres:16 # Auto-remove on stop: docker run --rm alpine echo "hello"
docker run = create + start. -d runs in the background. -it gives an interactive terminal. -e sets env vars. --rm removes on stop. -p publishes ports. It is the most used command.
Logs
# View logs: docker logs web # Follow (real time): docker logs -f web # Last 50 lines: docker logs --tail 50 web # With timestamps: docker logs -t web # Since a timestamp: docker logs --since "2024-01-01T00:00:00" web # Stderr only: docker logs web 2>&1 | grep ERROR
docker logs shows the container stdout/stderr. -f follows (like tail -f). --tail limits lines. -t adds timestamps. --since filters by date. Essential for application debugging.
Stats and Top
# Real-time resources (all): docker stats # Specific container: docker stats web db # Without stream (snapshot): docker stats --no-stream # Processes inside the container: docker top web docker top web -ef # ps format # Disk usage: docker system df -v
docker stats shows CPU, RAM, network and I/O in real time. --no-stream gives a snapshot (useful in scripts). docker top lists internal processes. system df shows space used by images, containers and volumes.
List Containers (ps)
# Active only:
docker ps
# All (including stopped):
docker ps -a
# Custom format:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# Filter by state:
docker ps --filter "status=exited"
docker ps --filter "ancestor=nginx"
# Last created:
docker ps -ldocker ps lists active containers. -a includes stopped ones. --format customizes the output. --filter filters by state, image, name. -l shows the last one created. Essential for daily monitoring.
Inspect (Details)
# Full JSON:
docker inspect web
# Specific fields (Go template):
docker inspect --format="{{.State.Status}}" web
docker inspect --format="{{.NetworkSettings.IPAddress}}" web
docker inspect --format="{{.Config.Image}}" web
# Network IP:
docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" webdocker inspect returns JSON with the full configuration: state, network, mounts, env. --format extracts fields with Go templates. Shows IP, ports, volumes, restart policy. Essential troubleshooting tool.
Rename, Pause and Wait
# Rename: docker rename web old-web # Pause (freeze processes): docker pause web docker unpause web # Wait until it stops (returns exit code): docker wait web # Commit (container → image): docker commit web my-app:snapshot
rename changes the name. pause freezes processes (SIGSTOP) without stopping. wait blocks until the container ends. commit creates an image of the current state — avoid in production; use a Dockerfile.
Start, Stop, Restart, Kill
docker start web # start a stopped container docker stop web # stop (SIGTERM, 10s, SIGKILL) docker restart web # stop + start docker kill web # immediate SIGKILL (force) # With a custom timeout: docker stop -t 30 web # wait 30s before kill # Multiple: docker stop web api db docker start $(docker ps -aq) # start all
stop sends SIGTERM and waits 10s before SIGKILL. kill is immediate (no graceful shutdown). restart = stop + start. -t adjusts the timeout. Accepts names or IDs, multiple at once.
Remove Containers
# Remove a stopped one: docker rm web # Force (even active): docker rm -f web # Remove on stop (automatic): docker run --rm alpine echo "temporary" # Clean all stopped ones: docker container prune # Remove all (active and stopped): docker rm -f $(docker ps -aq)
docker rm removes stopped containers. -f forces removal of active ones (kill + rm). --rm on run removes automatically on exit. container prune cleans all stopped ones. Careful with $(docker ps -aq).
Exec (Commands in the Container)
# Run a command in an active container: docker exec web nginx -t # Interactive shell: docker exec -it web /bin/sh docker exec -it db psql -U postgres # As root: docker exec -u root -it web /bin/sh # With an environment variable: docker exec -e DEBUG=true web env
docker exec runs commands inside a running container. -it for interactive sessions. -u sets the user. Does not restart the container. Ideal for debugging, checking configs and accessing shells.
Copy Files (cp)
# Host → Container: docker cp ./config.yml web:/etc/app/config.yml # Container → Host: docker cp web:/var/log/app.log ./logs/ # Full directory: docker cp ./src web:/app/src # Syntax: docker cp SOURCE TARGET # Container name or ID + absolute path
docker cp copies files between host and container. Works with active or stopped containers. Syntax: container:/path. No tar needed — copies directly. Useful for quick configs and log extraction.
Networks
List and Create Networks
# List networks: docker network ls # Create a bridge network: docker network create my-network # Create with a custom subnet: docker network create --subnet 172.20.0.0/16 --gateway 172.20.0.1 app-net # Details: docker network inspect my-network
docker network ls shows networks (bridge, host, none by default). create creates a custom bridge network. --subnet sets the IP range. Custom networks allow automatic DNS between containers.
Host Network
# Container shares the host network (no NAT): docker run -d --network host nginx # No need for -p (uses host ports directly) # Nginx accessible at localhost:80 # Limitations: # • No network isolation # • Does not work in Docker Desktop (Mac/Win) # • Ideal for maximum performance or debugging
--network host removes network isolation — the container uses the host interfaces directly. No NAT, no -p. Best performance but no security. Only works on Linux. Useful for network tools and debugging.
Network Inspect
docker network inspect app-net
# Useful fields:
docker network inspect --format="{{range .Containers}}{{.Name}} {{end}}" app-net
# A container IP:
docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" web
# Gateway and subnet:
docker network inspect --format="{{(index .IPAM.Config 0).Subnet}}" app-netnetwork inspect shows subnet, gateway, connected containers and IPs. --format extracts specific data. Shows driver, options and labels. Essential for debugging connectivity between services.
Connect a Container to a Network
# On creation: docker run -d --name api --network my-network my-app # Connect an existing container: docker network connect my-network web # Disconnect: docker network disconnect my-network web # Verify: docker network inspect my-network
--network on run connects to the network at creation. connect/disconnect manage connections at runtime. A container can be on multiple networks. On the same network, containers communicate by name (internal DNS).
None Network (Isolation)
# No network (loopback only): docker run --network none alpine ip addr # Result: only the lo interface (127.0.0.1) # No external access, no DNS # Use cases: # • Secure processing (no internet) # • Isolation tests # • Jobs that do not need network
--network none removes all connectivity. The container is fully isolated (loopback only). Ideal for sensitive processing, sandboxes and tests. No port works. Maximum security through isolation.
IPv6 and Macvlan Networks
# Network with IPv6: docker network create --ipv6 --subnet 2001:db8::/64 net-v6 # Macvlan (container with an IP on the physical network): docker network create -d macvlan \ --subnet 192.168.1.0/24 \ --gateway 192.168.1.1 \ -o parent=eth0 macvlan-net docker run -d --network macvlan-net --ip 192.168.1.200 my-service
--ipv6 enables IPv6 on the network. macvlan gives the container an IP from the physical network (like a real machine). No NAT, no port mapping. Ideal for servers that need a direct IP on the LAN. Requires a physical interface.
Publish Ports (-p)
# host:container docker run -d -p 8080:80 nginx # TCP docker run -d -p 5353:53/udp dns-app # UDP docker run -d -p 127.0.0.1:3000:3000 app # localhost only # Multiple ports: docker run -d -p 80:80 -p 443:443 web # Random port on the host: docker run -d -p 80 nginx docker port web # see the mapping
-p host:container exposes ports. Without an IP, it listens on all interfaces. 127.0.0.1: restricts to localhost. /udp for UDP. docker port shows mappings. Without -p, ports stay internal.
Remove and Clean Networks
# Remove a network (no connected containers): docker network rm my-network # Remove all unused ones: docker network prune # Force (disconnects containers): docker network rm -f my-network # Default networks (bridge, host, none) cannot be removed
network rm removes networks without containers. prune cleans all unused ones. Default networks (bridge, host, none) are permanent. Disconnect containers before removing or use -f.
DNS Between Containers
# Create the network and containers: docker network create app-net docker run -d --name db --network app-net postgres docker run -d --name api --network app-net my-api # Inside "api", resolve "db" by name: docker exec api ping db # it works! docker exec api getent hosts db # db IP # Automatic DNS only on custom networks (not the default bridge)
On custom bridge networks, Docker enables internal DNS. Containers resolve each other by name (e.g. ping db). On the default network, DNS does not work — it needs --link (deprecated). Always use custom networks.
Network Alias
# Container with multiple DNS names: docker run -d --name db1 --network app-net --network-alias database postgres # Other containers resolve both: # ping db1 → works # ping database → works (alias) # Useful for service discovery: docker run -d --name api1 --network app-net --network-alias api my-app docker run -d --name api2 --network app-net --network-alias api my-app
--network-alias adds extra DNS names. Multiple containers with the same alias = round-robin DNS (simple load balancing). Ideal for service discovery without external tools. Works on custom networks.
Docker Compose
docker-compose.yml Structure
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- api
api:
build: ./api
environment:
- DB_HOST=db
- DB_PORT=5432
depends_on:
- db
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
volumes:
pgdata:YAML file with a services section (containers), volumes (data) and networks (optional). Each service has image or build. depends_on sets the startup order. Compose creates an automatic network.
Networks in Compose
services:
frontend:
image: nginx
networks:
- frontend-net
api:
build: ./api
networks:
- frontend-net
- backend-net
db:
image: postgres:16
networks:
- backend-net
networks:
frontend-net:
backend-net:Services only communicate on the same network. The example isolates: frontend talks to api, api talks to db, but frontend cannot reach db directly. Compose creates a default network automatically if none is defined.
Compose Watch (Hot-Reload)
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
# Enable:
# docker compose watch
# or: docker compose up --watch -ddocker compose watch (Compose 2.22+) syncs changes in real time. action: sync copies files without a rebuild. action: rebuild rebuilds the image. Replaces bind mounts in many cases. Faster and more reliable.
Essential Commands
docker compose up -d # create and start (background) docker compose down # stop and remove everything docker compose ps # list services docker compose logs -f # real-time logs docker compose exec web sh # shell in a service docker compose build # rebuild images docker compose pull # pull all images docker compose restart api # restart a service
up -d creates the network, volumes and starts everything. down removes containers and the network (volumes remain). logs -f follows all services. exec opens a shell. build rebuilds images with a local build.
Profiles and Overrides
# docker-compose.yml with profiles: # services: # debug-tools: # profiles: ["debug"] # image: nicolaka/netshoot # Start only the base services: docker compose up -d # Include the debug profile: docker compose --profile debug up -d # Override for development: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
profiles enable services conditionally. Without --profile, services with a profile do not start. -f layers files (override). Pattern: base + dev/prod override. The second file overrides the first.
Logs and Debugging
# Logs from all services: docker compose logs -f # Specific service: docker compose logs -f api # Last N lines: docker compose logs --tail 50 db # Exec for debugging: docker compose exec api sh docker compose exec db psql -U postgres # Validated config: docker compose config
compose logs -f aggregates logs from all services. --tail limits lines. exec opens a shell in a service. compose config validates and shows the resolved YAML (with overrides and .env applied).
Build with Compose
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
args:
NODE_ENV: production
target: production # multi-stage
ports:
- "3000:3000"
# Or simply:
worker:
build: ./workerbuild can be a string (path) or an object with context, dockerfile, args. target selects a stage in multi-stage builds. docker compose build rebuilds. up --build does build + up.
# docker-compose.yml
services:
app:
image: my-app:${TAG:-latest}
environment:
- DATABASE_URL=${DB_URL}
- SECRET=${SECRET_KEY}
# .env (same directory, loaded automatically):
TAG=2.1.0
DB_URL=postgres://user:pass@db:5432/app
SECRET_KEY=super-secretCompose loads .env automatically for variable substitution. ${VAR:-default} sets a fallback. environment passes vars to the container. Never commit .env with secrets — use .env.example.
Scale Services
# Scale (multiple replicas): docker compose up -d --scale worker=3 # See the replicas: docker compose ps # Logs from all replicas: docker compose logs -f worker # Limitation (no fixed port when scaling): # ports: do NOT use "8080:80" if scaling # Use a range: "8080-8082:80" or no ports
--scale worker=3 creates 3 replicas of the service. It does not work with fixed ports (conflict). Use a port range or internal service discovery. Ideal for workers and parallel processing. Load balancing via DNS.
depends_on and healthcheck
services:
app:
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
redis:
image: redis:7-alpinedepends_on with condition: service_healthy waits for the healthcheck to pass before starting dependents. healthcheck defines the command, interval and retries. service_started only waits for startup (without checking health).
Restart Policies
services:
app:
image: my-app
restart: unless-stopped
db:
image: postgres:16
restart: always
worker:
image: worker-app
restart: on-failure:3 # 3 attempts
# Options:
# no → never restarts (default)
# always → always (even after a manual stop)
# on-failure → only if exit code != 0
# unless-stopped → always except a manual stoprestart sets the restart policy. unless-stopped is the most used (restarts unless stopped manually). on-failure:N limits attempts. always restarts even after docker restart. Essential in production.
Dockerfile
FROM and Base Images
# Official base image: FROM node:20-alpine # With a specific version: FROM python:3.12-slim # Minimal image: FROM alpine:3.19 # Scratch (empty, for binaries): FROM scratch # Choice: alpine (5MB) < slim (30MB) < full (300MB+) # Alpine uses musl libc (may have incompatibilities)
FROM sets the base image (first instruction). alpine is minimal (~5MB). slim removes unnecessary tools. scratch is empty (for Go/Rust binaries). Choose the smallest one that works for your app.
WORKDIR and EXPOSE
# Set the working directory: WORKDIR /app # Everything below is relative to /app: COPY package.json . RUN npm install COPY . . # Document the port (does NOT publish!): EXPOSE 3000 EXPOSE 5432/tcp EXPOSE 53/udp # Actually publish: docker run -p 3000:3000
WORKDIR sets the directory (creates it if missing). Avoids cd in RUN. EXPOSE is documentation — it does not publish the port. Real publishing is via -p on run or ports in Compose. Always use WORKDIR.
.dockerignore
# .dockerignore (at the root of the build context) node_modules .git .env *.md dist/ coverage/ .vscode docker-compose*.yml Dockerfile* # Reduces the build context (faster) # Avoids copying secrets (.env) # Similar to .gitignore
.dockerignore excludes files from the build context. Reduces build time (less data sent to the daemon). Avoids copying .env, node_modules, .git. Same syntax the .gitignore. Always create one.
RUN (Execute Commands)
# Install dependencies:
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Multiple commands in one layer:
RUN npm ci --production \
&& npm cache clean --force
# Alpine:
RUN apk add --no-cache python3 make g++RUN executes commands during the build. Each RUN creates a layer — combine with && to minimize. rm -rf /var/lib/apt/lists/* cleans the cache. --no-cache on apk avoids caching. Fewer layers = smaller image.
CMD and ENTRYPOINT
# CMD: default command (can be overridden) CMD ["node", "server.js"] # ENTRYPOINT: fixed binary (args are passed) ENTRYPOINT ["python", "app.py"] # Combine (entrypoint + default args): ENTRYPOINT ["nginx"] CMD ["-g", "daemon off;"] # Override CMD on run: # docker run my-app --debug
CMD sets the default command (replaceable on run). ENTRYPOINT is fixed — arguments are appended. Combination: ENTRYPOINT the the binary, CMD the default args. The JSON format ["exec"] is preferred (no shell wrapper).
LABEL and STOPSIGNAL
# Metadata:
LABEL maintainer="dev@company.com"
LABEL version="2.1.0"
LABEL description="Customer management API"
# Visible in: docker inspect --format="{{.Config.Labels}}"
# Stop signal:
STOPSIGNAL SIGTERM
# For apps that use SIGQUIT:
STOPSIGNAL SIGQUITLABEL adds metadata (author, version, description). Visible via inspect. Useful for cataloging images. STOPSIGNAL sets the shutdown signal (default: SIGTERM). Nginx uses SIGQUIT for a graceful stop.
COPY and ADD
# Copy files (preferred): COPY package*.json ./ COPY src/ ./src/ COPY config.yml /etc/app/ # ADD (extracts tar automatically): ADD app.tar.gz /opt/app/ # From a URL (ADD only): ADD https://example.com/file.zip /tmp/ # With ownership: COPY --chown=node:node . /app
COPY copies local files (transparent, preferred). ADD adds features: tar extraction and URL download. --chown sets the owner. Copy only what is needed — each COPY invalidates the cache if the file changes.
USER and Permissions
# Create a non-root user: RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Copy with ownership: COPY --chown=appuser:appgroup . /app # Switch to non-root: USER appuser # From here on, everything runs the appuser: RUN whoami # appuser CMD ["node", "server.js"]
USER sets the user for subsequent commands. Never run the root in production. Create a user with adduser -S (Alpine) or useradd -r (Debian). --chown on COPY ensures correct permissions.
ENV and ARG
# ENV: runtime variable (persists in the container)
ENV NODE_ENV=production
ENV APP_PORT=3000
# ARG: variable only during the build
ARG VERSION=1.0.0
ARG BUILD_DATE
# Use ARG in RUN:
RUN echo "Building v${VERSION}"
# ARG does not exist at runtime!
# To pass it to runtime: ENV MY_VAR=$ARG_VARENV defines variables that exist in the container at runtime. ARG only exists during the build (does not persist). Use ARG for versions and build configs. ENV for application configs. Both accessible via $VAR.
HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:3000/health || exit 1 # Without curl (Alpine): HEALTHCHECK CMD wget -qO- http://localhost:80/ || exit 1 # States: starting → healthy / unhealthy # Visible in: docker ps (STATUS column)
HEALTHCHECK defines a health check. --interval frequency, --timeout limit, --retries failures until unhealthy. --start-period gives time to start. Docker and Compose use it for restart and depends_on.
Registry and Distribution
Docker Hub (push/pull)
# Login: docker login # Tag with username: docker tag my-app:1.0 myuser/my-app:1.0 # Push: docker push myuser/my-app:1.0 # Pull on another server: docker pull myuser/my-app:1.0 # Logout: docker logout
docker login authenticates with Docker Hub. The tag must include username/. push uploads layers. pull downloads. Public images are free; private ones are limited on the free plan. Use tokens in CI/CD.
Docker Content Trust
# Enable image signing: export DOCKER_CONTENT_TRUST=1 # Push now signs automatically: docker push myuser/app:1.0 # Pull verifies the signature: docker pull myuser/app:1.0 # Disable temporarily: docker pull --disable-content-trust myuser/app:1.0
Docker Content Trust (DCT) signs and verifies images. Guarantees integrity and publisher. Enable with DOCKER_CONTENT_TRUST=1. Push creates a signature; pull verifies it. Protects against tampered images.
Vulnerability Scanning
# Docker Scout (successor of docker scan): docker scout cves my-app:1.0 # Quick summary: docker scout quickview my-app:1.0 # Compare versions: docker scout diff my-app:1.0 my-app:1.1 # Trivy (open-source alternative): docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy image my-app:1.0
docker scout analyzes CVEs in the image. quickview gives a summary. diff compares between versions. Trivy is an open-source alternative. Integrate it in CI/CD to block images with critical vulnerabilities.
Private Registry
# Run a local registry: docker run -d -p 5000:5000 --name registry registry:2 # Tag for the local registry: docker tag my-app localhost:5000/my-app:1.0 # Push: docker push localhost:5000/my-app:1.0 # Pull: docker pull localhost:5000/my-app:1.0 # List images: curl http://localhost:5000/v2/_catalog
registry:2 is the official self-hosted registry. Runs on port 5000. The tag includes host:port/. No auth by default (add TLS + htpasswd in production). REST API at /v2/ for management.
Multi-registry Images
# Tag for multiple registries: docker tag app:1.0 ghcr.io/user/app:1.0 docker tag app:1.0 registry.gitlab.com/user/app:1.0 docker tag app:1.0 123456.dkr.ecr.us-east-1.amazonaws.com/app:1.0 # Push to all of them: docker push ghcr.io/user/app:1.0 docker push registry.gitlab.com/user/app:1.0 # Same IMAGE ID, different registries
An image can have tags in multiple registries. Same IMAGE ID, different destinations. Useful for mirroring (redundancy) or migration. Each push only sends layers the registry does not have (deduplication).
Manifest and Attestation
# See the manifest (platforms, layers): docker manifest inspect nginx:latest # Create a manual manifest list: docker manifest create myuser/app:1.0 \ myuser/app:1.0-amd64 \ myuser/app:1.0-arm64 docker manifest push myuser/app:1.0 # Attestation (SBOM, provenance): docker buildx build --sbom=true --provenance=true -t app .
The manifest describes an image platforms and layers. manifest create joins multi-arch images. attestation adds SBOM and provenance to the build. Increases transparency and security in the supply chain.
Login to Registries
# Docker Hub: docker login # GitHub Container Registry: echo $TOKEN | docker login ghcr.io -u USER --password-stdin # AWS ECR: aws ecr get-login-password | docker login --username AWS --password-stdin ID.dkr.ecr.REGION.amazonaws.com # Google GCR: gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin gcr.io # Azure ACR: az acr login --name myregistry
Each cloud has its own login method. --password-stdin keeps secrets out of the history. ghcr.io uses a PAT. ECR uses a temporary token from the AWS CLI. Credentials are stored in ~/.docker/config.json.
Registry with Authentication
# Create the password file: docker run --rm --entrypoint htpasswd httpd:2 -Bbn user pass > auth/htpasswd # Registry with auth + TLS: docker run -d -p 5000:5000 \ -v $(pwd)/auth:/auth \ -e REGISTRY_AUTH=htpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALM="Registry" \ -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ -v certs:/certs \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \ -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \ registry:2
A production registry needs TLS + htpasswd. Create the file with htpasswd -Bbn. Configure via environment variables. Without TLS, Docker refuses the push (insecure-registry is for dev only).
Tags and Versioning
# Tag best practices: docker tag app my-app:1.2.3 # semver docker tag app my-app:latest # latest docker tag app my-app:$(git rev-parse --short HEAD) # git SHA # Push multiple tags: docker push myuser/app:1.2.3 docker push myuser/app:latest # List tags (Docker Hub API): curl "https://hub.docker.com/v2/repositories/myuser/app/tags"
Versioning: use semver (1.2.3) + latest for the most recent. In CI/CD, tag with the git SHA for traceability. Never use only latest in production — it can change unexpectedly.
Garbage Collection
# Clean unreferenced blobs in the registry: docker exec registry registry garbage-collect /etc/docker/registry/config.yml # Stop the registry first (safer): docker stop registry docker run --rm -v registry-data:/var/lib/registry registry:2 \ garbage-collect /etc/docker/registry/config.yml docker start registry # Check the space: docker system df
garbage-collect removes orphaned blobs from the registry (layers without a tag). Happens after deleting tags. It does not free space immediately — it needs GC. Run it with the registry stopped for consistency.
System and Maintenance
docker info and version
# Daemon information: docker info # Shows: Containers, Images, Storage Driver, # Cgroup, Kernel, OS, CPUs, Total Memory # Version (client + server): docker version # Short version only: docker --version
docker info shows the full state: number of containers, images, storage driver, memory, CPUs. docker version shows the client and server versions. The first command to diagnose problems.
Real-Time Events
# All events:
docker events
# Filter:
docker events --filter "type=container"
docker events --filter "event=start"
docker events --filter "image=nginx"
# With a format:
docker events --format "{{.Time}} {{.Action}} {{.Actor.Attributes.name}}"
# Since/until:
docker events --since "2024-01-01" --until "2024-01-02"docker events streams daemon events: create, start, stop, die, pull. --filter by type, event or image. Useful for auditing, alerts and automation. Shows timestamp, action and metadata.
Common Troubleshooting
# "Cannot connect to Docker daemon"
sudo systemctl start docker
# "permission denied"
suusermod -aG docker $USER && newgrp docker
# "port already in use"
sudo lsof -i :8080 # see who uses the port
# "no space left on device"
docker system prune -af
# Container does not start:
docker logs container
docker inspect --format="{{.State.Error}}" containerCommon errors: stopped daemon (systemctl start), permissions (usermod), busy port (lsof), full disk (prune). Always check docker logs and inspect for error details.
Disk Usage (system df)
# Summary: docker system df # Detailed: docker system df -v # Output: # TYPE TOTAL ACTIVE SIZE RECLAIMABLE # Images 15 3 12.5GB 10.2GB (81%) # Containers 5 2 1.2GB 800MB (66%) # Local Volumes 8 3 500MB 200MB (40%) # Build Cache 30 0 3GB 3GB
docker system df shows space by type. RECLAIMABLE indicates what can be cleaned. -v lists each item. Build Cache accumulates a lot in CI/CD. Check regularly to avoid a full disk.
Restart the Daemon Safely
# Without live-restore: all containers stop!
sudo systemctl restart docker
# With live-restore (daemon.json):
# { "live-restore": true }
# → containers keep running during the restart
# Verify after the restart:
docker ps
docker info
# Reload the config without a restart:
sudo systemctl reload dockerrestart docker stops all containers (without live-restore). Enable live-restore: true in daemon.json to keep containers running. reload applies the config without restarting. Always check docker ps afterwards.
Update Without Downtime
# 1. Enable live-restore in daemon.json:
# { "live-restore": true }
sudo systemctl reload docker
# 2. Update the packages:
sudo apt-get install docker-ce docker-ce-cli containerd.io
# 3. Restart (containers keep running):
sudo systemctl restart docker
# 4. Verify:
docker ps # all still active?
docker info # new version?With live-restore: true, containers survive the daemon restart. Update packages and restart — apps keep running. Without live-restore, there is downtime. In production with Swarm/K8s, do a rolling update.
Prune (General Cleanup)
# Clean everything not in use: docker system prune # Include volumes (CAREFUL - data!): docker system prune --volumes # Include images without containers: docker system prune -a # Without confirmation: docker system prune -af # Build cache only: docker builder prune
docker system prune removes stopped containers, orphaned networks and dangling images. -a includes images without containers. --volumes removes volumes (dangerous!). builder prune cleans the build cache. Run it periodically.
Limit Container Logs
# Global (daemon.json):
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
# Per container:
docker run -d --log-opt max-size=10m --log-opt max-file=3 app
# See the log sizes:
sudo du -sh /var/lib/docker/containers/*/*-json.logLogs can fill the disk! Configure max-size and max-file in daemon.json (global) or per container. json-file is the default driver. Alternatives: syslog, fluentd, local.
Daemon Logs
# Linux (systemd): sudo journalctl -u docker.service -f sudo journalctl -u docker.service --since "1 hour ago" # Check for errors: sudo journalctl -u docker.service | grep -i error # Docker Desktop (Mac/Win): # Settings → Troubleshoot → View logs # Container logs: docker logs --tail 100 -f container-name
On Linux, daemon logs via journalctl -u docker.service. Shows startup errors, OOM, network issues. In Docker Desktop, logs are in the UI. For containers: docker logs. Essential for troubleshooting.
Storage Drivers
# See the current driver:
docker info | grep "Storage Driver"
# Configure (daemon.json):
{ "storage-driver": "overlay2" }
# Available drivers:
# overlay2 → recommended (Linux 4.0+)
# fuse-overlayfs → rootless
# btrfs/zfs → advanced (native snapshots)
# Check the support:
grep overlay /proc/filesystemsoverlay2 is the recommended storage driver. Uses layers with copy-on-write. fuse-overlayfs for rootless. Never change the driver with existing data (you lose everything). Check kernel support first.
Advanced
Multi-stage Builds
# Stage 1: Build FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Stage 2: Production (minimal image) FROM nginx:alpine AS production COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Multi-stage uses multiple FROMs. Copies only artifacts with --from=builder. The final image has no node_modules, source, etc. Reduces from ~1GB to ~25MB. An essential pattern for production.
Init and Signal Handling
# Problem: PID 1 does not manage zombies/signals # Solution: use init: docker run -d --init my-app # Or in the Dockerfile: # ENTRYPOINT ["docker-init", "--", "node", "server.js"] # Check the signals: docker stop app # sends SIGTERM # The app must handle SIGTERM for a graceful shutdown # Without init: zombie processes accumulate
--init injects tini the PID 1. Manages signals and zombie processes correctly. Without init, the app process is PID 1 and can ignore SIGTERM. Always use --init or handle signals in the app.
Optimize Images
# 1. Minimal base: FROM node:20-alpine # 2. Order for cache (changes less → first): COPY package*.json ./ RUN npm ci --production COPY . . # 3. Multi-stage (do not carry the source): FROM alpine AS runtime COPY --from=builder /app/dist ./dist # 4. Combine RUN: RUN apk add --no-cache curl && rm -rf /var/cache/apk/* # 5. .dockerignore (no node_modules, .git)
Optimization: alpine base, multi-stage, COPY order for cache, combine RUN, .dockerignore. Copy package.json before the source (npm install cache). Goal: smaller image, faster build.
BuildKit and Advanced Cache
# syntax=docker/dockerfile:1
# npm cache between builds:
RUN --mount=type=cache,target=/root/.npm \
npm ci --production
# apt cache:
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y curl
# Secret in the build (without staying in the image):
RUN --mount=type=secret,id=npm_token \
npm ci --registry=https://$(cat /run/secrets/npm_token)@npm.pkg.github.comBuildKit allows --mount=type=cache (persistent cache between builds) and --mount=type=secret (secrets without persisting in the image). Parallel and faster builds. Enabled by default since Docker 23+.
docker buildx
# Create a builder: docker buildx create --name my-builder --use # Multi-platform build: docker buildx build --platform linux/amd64,linux/arm64 \ -t myuser/app:1.0 --push . # Build with local output: docker buildx build --platform linux/arm64 -o type=docker -t app:arm . # Inspect the builder: docker buildx inspect --bootstrap # Remove: docker buildx rm my-builder
buildx extends the build: multi-platform, remote cache, custom output. --platform sets the architectures. --push sends straight to the registry. -o type=docker loads locally. Essential for ARM + AMD.
Docker in CI/CD
# GitHub Actions (example):
# - name: Build & Push
# run: |
# docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
# docker build -t user/app:${{ github.sha }} .
# docker push user/app:${{ github.sha }}
# Layer cache:
docker build --cache-from user/app:latest -t user/app:new .
# Test before pushing:
docker run --rm user/app:new npm test
# Only push if the tests passIn CI/CD: build with the git SHA tag, test, and only then push. --cache-from uses the previous image the cache. Login with secrets (never hardcoded). --rm cleans the test container. The standard for reliable deploys.
Docker Swarm (Basics)
# Start the swarm: docker swarm init --advertise-addr 192.168.1.10 # Create a service: docker service create --name web --replicas 3 -p 80:80 nginx # List services: docker service ls docker service ps web # tasks # Scale: docker service scale web=5 # Rolling update: docker service update --image nginx:1.25 web
Swarm is Docker native orchestration. swarm init creates the cluster. service create with --replicas distributes across nodes. scale adjusts replicas. update does a rolling update. Simpler than Kubernetes.
Entrypoint Scripts
# entrypoint.sh #!/bin/sh set -e # Wait for the DB: until pg_isready -h db -p 5432; do echo "Waiting for the DB..." sleep 2 done # Run migrations: npm run migrate # Execute the main command: exec "$@"
Entrypoint scripts prepare the environment before the app. Wait for dependencies, run migrations, configure env. exec "$@" replaces the shell with the CMD (receives signals). set -e stops on errors. A production standard.
Advanced Healthcheck
# In Compose with depends_on:
# app depends on db being healthy
# See the state:
docker inspect --format="{{.State.Health.Status}}" app
docker inspect --format="{{range .State.Health.Log}}{{.Output}}{{end}}" app
# States: starting → healthy | unhealthy
# Without a healthcheck in the Dockerfile (on run):
docker run -d --health-cmd="curl -f http://localhost/ || exit 1" \
--health-interval=30s --health-retries=3 appHealth.Status shows starting/healthy/unhealthy. Health.Log has the check output. Compose uses it for depends_on condition. Can be defined in the Dockerfile or on run. Essential for auto-healing.
Docker API
# Via Unix socket:
curl --unix-socket /var/run/docker.sock http://localhost/v1.45/containers/json
# List containers:
curl --unix-socket /var/run/docker.sock http://localhost/containers/json?all=true
# Create a container:
curl --unix-socket /var/run/docker.sock -X POST \
-H "Content-Type: application/json" \
-d '{"Image":"alpine","Cmd":["echo","hello"]}' \
http://localhost/containers/create
# Info:
curl --unix-socket /var/run/docker.sock http://localhost/infoDocker exposes a REST API via /var/run/docker.sock. Everything the CLI does, the API does. Version: /v1.45/. Used by tools (Portainer, Traefik). Careful: socket access = root access.
Installation and Setup
Install on Linux (Ubuntu/Debian)
# Remove old versions sudo apt-get remove docker docker-engine docker.io # Install dependencies sudo apt-get update sudo apt-get install ca-certificates curl gnupg # Add the official GPG key sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # Add the repository echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list # Install Docker Engine sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Installs Docker Engine on Ubuntu/Debian via the official repository. The docker-compose-plugin includes Compose V2. Always use the official repository to receive security updates.
First Container (hello-world)
docker run hello-world # What happens: # 1. Docker looks for the image locally # 2. Not found → pulls from Docker Hub # 3. Creates a container from the image # 4. Runs the binary → prints a message # 5. Container ends (exit 0) docker ps -a # see the container (Exited state)
hello-world demonstrates the full cycle: pull → create → run → exit. The container stops after running. docker ps -a shows it the Exited. Foundation for understanding the container model.
Update Docker
# Linux (Ubuntu/Debian): sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin # Check the version: docker --version # Windows/Mac: # Docker Desktop → Settings → Software Updates # Or: winget upgrade Docker.DockerDesktop (Windows) # Release notes: # https://docs.docker.com/engine/release-notes/
On Linux, update via apt-get (same command the the install). On Windows/Mac, Docker Desktop updates automatically or via winget. Keep it updated for security patches and new features.
Install on Windows and Mac
# Windows / Mac: Docker Desktop # Download: https://www.docker.com/products/docker-desktop/ # Windows: requires WSL2 (Windows Subsystem for Linux) wsl --install # enable WSL2 # Then install Docker Desktop (.exe installer) # Mac (Apple Silicon or Intel): # Download .dmg → drag to Applications # Verify the installation: docker --version docker compose version
Docker Desktop is the recommended way for Windows and Mac. On Windows it uses WSL2 the backend (faster than Hyper-V). On Mac it uses a lightweight VM. It includes Docker Engine, Compose and BuildKit.
docker run (First Server)
# Nginx in the background, port 8080 → 80 docker run -d --name web -p 8080:80 nginx # Open in the browser: http://localhost:8080 # View logs: docker logs web # Stop and remove: docker stop web docker rm web
docker run -d runs in the background (detached). -p 8080:80 maps host→container port. --name gives a friendly name. Nginx serves the default page at localhost:8080. First step towards serving applications.
Fundamental Concepts
# Image → read-only template (e.g. nginx:alpine) # Container → running instance of an image # Volume → persistent data outside the container # Network → communication between containers # Registry → image repository (Docker Hub) # Daemon → Docker service (dockerd) # Client → CLI that talks to the daemon (docker) # Architecture: Client → Daemon → Registry # docker run = pull + create + start
Image is the template; the container is the live instance. Volumes persist data. Networks connect containers. The daemon (dockerd) runs everything. The client (docker CLI) sends commands via REST API.
Verify Installation
docker --version # Docker version 27.x.x docker compose version # Docker Compose version v2.x.x docker info # daemon details docker run hello-world # full test # If "permission denied" on Linux: suusermod -aG docker $USER newgrp docker # apply without logout
docker --version confirms the installation. docker run hello-world tests the full flow (pull + create + run). On Linux, add the user to the docker group to avoid sudo.
Docker Service (systemd)
# Manage the daemon the a service: sudo systemctl start docker sudo systemctl stop docker sudo systemctl restart docker sudo systemctl status docker # Start automatically at boot: sudo systemctl enable docker # Check if it is active: systemctl is-active docker # "active"
On Linux, Docker runs the a systemd service. enable ensures it starts at boot. status shows state and PID. In Docker Desktop (Windows/Mac), the service is managed by the application automatically.
Configure the Daemon (daemon.json)
# /etc/docker/daemon.json (Linux)
# or Settings → Docker Engine (Docker Desktop)
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
"storage-driver": "overlay2",
"live-restore": true,
"default-address-pools": [
{ "base": "172.30.0.0/16", "size": 24 }
]
}
# Restart after changes:
sudo systemctl restart dockerThe daemon.json file configures Docker globally. log-opts limits logs (prevents a full disk). overlay2 is the recommended storage driver. live-restore keeps containers running when the daemon restarts.
Docker Context (Multiple Hosts)
# List contexts: docker context ls # Create a remote context: docker context create production --docker "host=ssh://user@192.168.1.100" # Switch context: docker context use production # Commands now run on the remote host: docker ps # lists the remote server containers # Back to local: docker context use default
Docker Context lets you manage multiple hosts without changing environment variables. create defines an endpoint (local or SSH). use switches the target. Ideal for managing remote servers with the same commands.
Volumes
Create and List Volumes
# Create a named volume: docker volume create db-data # List: docker volume ls # Filter: docker volume ls --filter "dangling=true" # Details: docker volume inspect db-data # Shows: Mountpoint, Driver, Labels, Scope
docker volume create creates a volume managed by Docker. ls lists all of them. dangling=true shows unused ones. Data lives in /var/lib/docker/volumes/. It survives container removal.
Share Data Between Containers
# Same volume in multiple containers: docker volume create shared docker run -d --name app1 -v shared:/data app docker run -d --name app2 -v shared:/data app # Both read/write in /data # Volume from another container (deprecated): docker run -d --volumes-from app1 new-app
Multiple containers can mount the same volume. Reads and writes are shared. Beware of concurrency (no locking). --volumes-from inherits mounts from another container (legacy). Prefer named volumes.
Volumes vs Bind Mounts
# Volume (managed by Docker): docker run -v data:/var/lib/mysql db # + Portable, easy backup, drivers # + Works the same on Linux/Mac/Win # Bind mount (host folder): docker run -v $(pwd)/src:/app/src app # + Hot-reload in development # - Depends on the host structure # - Permissions may conflict
Volumes are managed by Docker (portable, backup, drivers). Bind mounts link a host folder (hot-reload, debug). In production: volumes. In development: bind mounts for code. Never bind mount for database data.
Mount a Volume on Run
# Named volume: docker run -d -v db-data:/var/lib/postgresql/data postgres # Syntax: -v volume_name:/container/path # If the volume does not exist, Docker creates it automatically: docker run -d -v new-vol:/data alpine # With a specific driver: docker run -d --mount type=volume,dst=/data,volume-driver=local app
-v volume:/path mounts a named volume. If it does not exist, it is created automatically. Data persists across restarts and removals. --mount is the explicit syntax (preferred in production). The container path is absolute.
Volume Backup
# Backup: volume → tar on the host docker run --rm -v db-data:/source -v $(pwd):/backup alpine \ tar czf /backup/db-backup.tar.gz -C /source . # Restore: tar → volume docker run --rm -v db-data:/target -v $(pwd):/backup alpine \ tar xzf /backup/db-backup.tar.gz -C /target # Verify: docker run --rm -v db-data:/data alpine ls /data
Backup uses a temporary container with two mounts: source volume + destination folder. tar czf compresses. Restore reverses it: host tar → volume. --rm removes the container after the operation. An essential pattern for critical data.
Permissions and Ownership
# Container runs the UID 1000: docker run -d --user 1000:1000 -v data:/data app # Fix permissions in the Dockerfile: # RUN chown -R node:node /app/data # Check file ownership: docker exec app ls -la /data # Common problem: files created the root # Solution: --user or chown in the entrypoint
Common problem: files created the root inside the container. Solution: --user UID:GID on run. Or chown in the Dockerfile. Bind mounts inherit host permissions. New volumes are root by default.
Bind Mount (Host Folder)
# Bind mount: local folder → container docker run -d -v $(pwd)/src:/app/src node:20 # Windows: docker run -d -v C:\project\src:/app/src node:20 # Read-only: docker run -d -v ./config:/etc/app:ro my-app # Long syntax (recommended): docker run -d --mount type=bind,source="$(pwd)/src",target=/app/src app
Bind mount maps a host folder into the container. Changes are bidirectional and immediate. :ro makes it read-only. Ideal for development (hot-reload). --mount type=bind is more explicit and safer.
tmpfs Mount
# Mount in RAM (does not persist): docker run -d --tmpfs /tmp:rw,size=100m my-app # Long syntax: docker run -d --mount type=tmpfs,destination=/cache,tmpfs-size=52428800 app # Use cases: # • Fast temporary cache # • Sensitive data (never touches disk) # • Session files
tmpfs mounts in RAM — ultra-fast but volatile. Data disappears when the container stops. tmpfs-size limits the size in bytes. Ideal for cache, sessions and sensitive data that must not touch disk.
Remove and Clean Volumes
# Remove a volume: docker volume rm db-data # Remove container volumes when deleting it: docker rm -v my-container # Clean all unused ones: docker volume prune # Remove a specific volume from a container: docker rm -v --force container-with-data
volume rm removes a volume (data lost!). docker rm -v removes the container anonymous volumes. prune cleans dangling ones. Named volumes are NOT removed with rm -v — only with an explicit volume rm.
Volume Drivers
# Local driver (default): docker volume create --driver local my-data # With options (NFS, for example): docker volume create --driver local \ --opt type=nfs \ --opt o=addr=192.168.1.50,rw \ --opt device=:/exports/data \ nfs-vol # Use: docker run -d -v nfs-vol:/data my-app
The local driver is the default. It supports options such the NFS, tmpfs. Third-party drivers: Azure File, AWS EBS, Ceph. --opt passes driver settings. Enables network volumes shared between hosts.
Security and Resources
Limit Memory
# RAM limit: docker run -d --memory 512m my-app # With swap: docker run -d --memory 512m --memory-swap 1g app # Minimum reservation: docker run -d --memory-reservation 256m app # See the usage: docker stats my-app # OOM Kill (no limit): the kernel kills the process
--memory limits the maximum RAM. --memory-swap limits RAM+swap. --memory-reservation is a soft limit. Without a limit, a container can consume all the RAM. In production, always set limits.
Capabilities (Linux)
# Drop all and add only the needed ones:
docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE app
# See the current capabilities:
docker inspect --format="{{.HostConfig.CapAdd}}" app
docker inspect --format="{{.HostConfig.CapDrop}}" app
# Dangerous: grant all of them (equivalent to root):
docker run -d --privileged app # AVOID!--cap-drop ALL removes Linux privileges. --cap-add adds only what is needed (e.g. NET_BIND_SERVICE for port 80). --privileged gives full access — never in production. Principle of least privilege.
Network Isolation
# No external network (internal only): docker network create --internal isolated-net docker run -d --network isolated-net db # No internet access: docker run -d --network none app # Custom DNS: docker run -d --dns 8.8.8.8 --dns-search company.local app # Custom hostname: docker run -d --hostname my-server app
--internal creates a network without external access (no gateway). --network none isolates completely. --dns sets the DNS server. --hostname customizes the internal name. Combine them for layered security.
Limit CPU
# Limit to 1.5 CPUs: docker run -d --cpus 1.5 my-app # Percentage of 1 CPU: docker run -d --cpus 0.5 app # 50% of 1 core # Specific CPUs: docker run -d --cpuset-cpus "0,1" app # Relative weight (shares): docker run -d --cpu-shares 512 app # default: 1024
--cpus limits CPUs (accepts decimals). --cpuset-cpus pins to specific colors. --cpu-shares sets relative priority. Prevents a container from monopolizing AS CPU. Monitor with docker stats.
Secrets (Without Env Vars)
# Bad: secrets in env (visible in inspect): docker run -e DB_PASS=secret app # Good: mount the a file: echo "secret" > /tmp/db_pass.txt docker run -d --mount type=bind,source=/tmp/db_pass.txt,target=/run/secrets/db_pass,readonly app # Docker Swarm secrets: docker secret create db_pass ./db_pass.txt docker service create --secret db_pass my-app # Compose secrets: # secrets: # db_pass: # file: ./db_pass.txt
Avoid secrets in ENV (visible in inspect and logs). Mount them the a read-only file in /run/secrets/. In Swarm, use docker secret (encrypted in transit and at rest). In Compose, the secrets section.
PID and IPC Isolation
# Isolate the PID namespace (default): docker run -d my-app # already isolated # Share the host PID (debug): docker run -d --pid host diagnostic-tool # IPC isolation: docker run -d --ipc private my-app # Share memory (apps that use shared memory): docker run -d --ipc host my-app # UTS (hostname isolated by default): docker run -d --hostname app1 my-app
Docker isolates PID, IPC, UTS, mount and network by default (namespaces). --pid host removes the isolation (debug only). --ipc host for apps with shared memory. Keep the isolation in production.
Non-root User
# In the Dockerfile:
# RUN adduser -D appuser
# USER appuser
# On run (override):
docker run -d --user 1000:1000 my-app
# Check the current user:
docker exec app whoami
docker exec app id
# See the user configured in the image:
docker inspect --format="{{.Config.User}}" my-appRunning the root is a security risk. Set USER in the Dockerfile or --user on run. If an exploit happens, the attacker is limited to that user. Always use non-root in production.
Rootless Docker
# Install rootless: curl -fsSL https://get.docker.com/rootless | sh # Start the daemon: export DOCKER_HOST=unix:///run/user/1000/docker.sock dockerd-rootless.sh & # Verify: docker info | grep -i rootless # Advantages: # • The daemon does not run the root # • An exploit does not give access to the host
Rootless Docker runs the daemon the a normal user (no root). Even with an exploit, the attacker does not gain root on the host. Uses user namespaces. Limitations: no ports <1024, some networks unavailable. Recommended for security.
Read-only Filesystem
# Read-only filesystem: docker run -d --read-only my-app # With tmpfs for temporary writes: docker run -d --read-only --tmpfs /tmp --tmpfs /var/run app # Volume for persistent data: docker run -d --read-only -v data:/data app # Verify: docker exec app touch /test # "Read-only file system"
--read-only makes the filesystem immutable. Prevents malware from modifying files. Use --tmpfs for dirs that need writes (/tmp, /var/run). Combine with volumes for data. Maximum security.
Security Options
# No-new-privileges (prevents escalation): docker run -d --security-opt no-new-privileges:true app # AppArmor profile: docker run -d --security-opt apparmor=my-profile app # SELinux label: docker run -d --security-opt label:type:svirt_custom_t app # Custom seccomp profile: docker run -d --security-opt seccomp=profile.json app
no-new-privileges prevents setuid/escalation. AppArmor/SELinux apply MAC (mandatory access control). seccomp filters syscalls. Docker already applies default profiles — customize for sensitive apps.