Docker & Containers cheat sheet
160 commands from every lesson of Docker & Containers — Level by Level, on one page.
Run & look around
docker run -d --name web -p 8080:80 nginx:1.27 | Start a container in the background, publish port 8080 |
docker ps / docker ps -a | Running containers / all containers, including stopped |
docker logs -f --tail 50 web | Follow the last 50 log lines |
docker exec -it web sh | Open a shell inside a running container |
docker inspect web | Full JSON: config, IPs, mounts, state |
docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' web | Pick one field with a Go template |
docker stats --no-stream | CPU, memory and I/O per container |
docker top web | Processes inside a container |
Lifecycle
docker stop web && docker start web | Graceful stop (SIGTERM, then SIGKILL after 10 s) and start |
docker restart web | Stop + start |
docker rm -f web | Remove a container (force-stops it first) |
docker run --rm -it alpine:3.20 sh | Throwaway interactive container, removed on exit |
Engine info
docker version | Client and server (daemon) versions |
docker info | Storage driver, data root, cgroup driver, registries |
systemctl status docker | Is the daemon running? |
journalctl -u docker -n 100 | Daemon logs |
Build
docker build -t shop/api:1.0 . | Build from the Dockerfile in the current folder |
docker build -f docker/Dockerfile.prod -t shop/api:1.0 . | Use a Dockerfile with another name or path |
docker build --no-cache -t shop/api:1.0 . | Ignore the cache (fresh base layers, fresh package installs) |
docker build --target builder -t shop/api:build . | Stop at one stage of a multi-stage build |
docker build --build-arg VERSION=1.4 . | Pass a build argument (ARG) |
docker buildx build --platform linux/amd64,linux/arm64 -t reg/app:1 --push . | Multi-architecture build, pushed to a registry |
Inspect images
docker images / docker image ls | Local images and sizes |
docker history shop/api:1.0 | Layers and the instruction that made each one |
docker image inspect shop/api:1.0 | Config: CMD, ENTRYPOINT, USER, ENV, labels |
docker image prune | Remove dangling (untagged) images |
Dockerfile instructions
FROM image:tag AS name | Base image; name a stage for multi-stage builds |
COPY --from=builder /out/app /app | Copy files from an earlier stage |
RUN cmd | Run at build time; creates a layer |
ENTRYPOINT ["/app"] + CMD ["--port", "8080"] | Fixed program + default arguments |
USER 10001 | Run as a non-root user |
HEALTHCHECK CMD curl -f http://localhost:8080/healthz || exit 1 | Container health reported by Docker |
Networks
docker network create shopnet | User-defined bridge network (with DNS by container name) |
docker run -d --name db --network shopnet postgres:16 | Attach a container to a network |
docker network connect shopnet web | Attach a running container to another network |
docker network inspect shopnet | Subnet, gateway and attached containers |
docker port web | Which host ports are published |
docker run -p 127.0.0.1:8080:80 nginx | Publish only on localhost, not every interface |
docker run --network host nginx | Share the host's network stack (no isolation, no -p) |
Storage
docker volume create pgdata | Named volume managed by Docker |
docker run -v pgdata:/var/lib/postgresql/data postgres:16 | Mount a named volume |
docker run -v "$PWD/conf":/etc/nginx/conf.d:ro nginx | Bind-mount a host folder, read-only |
docker run --mount type=tmpfs,target=/tmp app | In-memory scratch space |
docker volume ls / docker volume inspect pgdata | List volumes / where the data lives |
docker volume prune | Remove volumes not used by any container (careful) |
Back up a volume
docker run --rm -v pgdata:/data -v "$PWD":/backup alpine tar czf /backup/pgdata.tgz -C /data . | Tar a volume's contents to the current folder |
Everyday commands
docker compose up -d | Create and start everything in the background |
docker compose up -d --build | Rebuild images that have a build: section, then start |
docker compose ps | Services, state and ports for this project |
docker compose logs -f api | Follow one service's logs |
docker compose exec db psql -U app | Run a command in a running service |
docker compose run --rm api python manage.py migrate | One-off command in a new container |
docker compose restart api | Restart one service |
docker compose down | Stop and remove containers and networks (volumes kept) |
docker compose down -v | Also delete named volumes: data is lost |
Check and debug
docker compose config | Show the final merged file with variables filled in |
docker compose pull | Pull newer images for all services |
docker compose top | Processes in each service |
docker compose -p shop -f compose.yaml up -d | Set the project name and file explicitly |
Health & order
docker compose ps | Shows (healthy) / (unhealthy) per service |
docker inspect --format '{{json .State.Health}}' shop-db-1 | Health-check history and output |
docker compose up -d --wait | Return only when services are running/healthy |
Files & profiles
docker compose -f compose.yaml -f compose.prod.yaml up -d | Merge a base file with an override |
docker compose --profile debug up -d | Also start services in the debug profile |
docker compose config | See the merged result before applying it |
Keep it tidy
docker compose logs --since 10m api | Recent logs of one service |
docker compose images | Images used by the project |
docker compose pull && docker compose up -d | Update to newer image tags / digests |
Tag, push, pull
docker tag shop/api:1.4 registry.lab.local:5000/shop/api:1.4 | Add a name that points at another registry |
docker push registry.lab.local:5000/shop/api:1.4 | Upload the image (only missing layers are sent) |
docker pull registry.lab.local:5000/shop/api:1.4 | Download it on another host |
docker pull nginx@sha256:<digest> | Pull an exact, immutable image by digest |
docker images --digests | Show local images with their digests |
docker buildx imagetools inspect nginx:1.27 | Digest and platforms of a remote image |
Log in
docker login registry.lab.local:5000 | Log in to a private registry (prompts for password) |
echo "$TOKEN" | docker login -u ci-bot --password-stdin ghcr.io | Non-interactive login for CI |
aws ecr get-login-password --region eu-west-1 | docker login -u AWS --password-stdin <acct>.dkr.ecr.eu-west-1.amazonaws.com | Log in to Amazon ECR |
docker logout registry.lab.local:5000 | Remove stored credentials |
cat ~/.docker/config.json | Where logins are stored (check for credsStore) |
Save / load without a registry
docker save -o api-1.4.tar shop/api:1.4 | Export an image to a tar file |
docker load -i api-1.4.tar | Import it on an air-gapped host |
Certificate & password
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes -keyout certs/domain.key -out certs/domain.crt -subj "/CN=registry.lab.local" -addext "subjectAltName=DNS:registry.lab.local,IP:192.168.56.10" | Self-signed certificate with SANs (lab) |
docker run --rm --entrypoint htpasswd httpd:2 -Bbn ci-bot 'S3cret!' > auth/htpasswd | Create a bcrypt htpasswd entry |
openssl x509 -in certs/domain.crt -noout -text | grep -A1 'Subject Alternative Name' | Check the SANs in a certificate |
Registry API
curl -u ci-bot https://registry.lab.local:5000/v2/_catalog | List repositories |
curl -u ci-bot https://registry.lab.local:5000/v2/shop/api/tags/list | List tags of one repository |
curl -u ci-bot -I -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json' https://registry.lab.local:5000/v2/shop/api/manifests/1.4 | Get the manifest digest (Docker-Content-Digest header) |
Clean up
curl -u ci-bot -X DELETE https://registry.lab.local:5000/v2/shop/api/manifests/sha256:<digest> | Delete a manifest (needs delete enabled) |
docker compose exec registry registry garbage-collect --delete-untagged /etc/docker/registry/config.yml | Free the disk space of deleted images (registry:2 path; registry:3 uses /etc/distribution/config.yml) |
Edit safely
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak | Back up before changing |
sudo dockerd --validate --config-file /etc/docker/daemon.json | Check the file without starting a daemon (Engine 23.0+) |
python3 -m json.tool /etc/docker/daemon.json | Quick JSON syntax check on older engines |
sudo systemctl reload docker | Apply reloadable settings (SIGHUP) without stopping containers |
sudo systemctl restart docker | Apply everything (containers stop unless live-restore is on) |
Verify
docker info | Logging driver, data root, mirrors, insecure registries, cgroup driver |
docker info -f '{{.LoggingDriver}} {{.DockerRootDir}}' | One or two fields |
docker info -f '{{json .RegistryConfig}}' | python3 -m json.tool | Mirrors and insecure registries in effect |
journalctl -u docker -n 50 --no-pager | Why the daemon failed to start |
systemctl cat docker | The unit file and drop-ins (look for -H or --config flags) |
Diagnose
openssl s_client -connect registry.lab.local:5000 -servername registry.lab.local -showcerts </dev/null | See the certificate chain the registry actually sends |
openssl x509 -in ca.crt -noout -subject -issuer -enddate -ext subjectAltName | Who issued it, when it expires, which names it covers |
curl -v https://registry.lab.local:5000/v2/ | Does the host's trust store accept it? |
docker info -f '{{json .RegistryConfig.IndexConfigs}}' | Which registries the daemon treats as insecure |
Trust one registry (Docker, no restart)
sudo mkdir -p /etc/docker/certs.d/registry.lab.local:5000 | Folder name = host:port exactly as in image names |
sudo cp ca.crt /etc/docker/certs.d/registry.lab.local:5000/ca.crt | CA (or self-signed cert) that signed the registry cert |
sudo cp client.cert client.key /etc/docker/certs.d/registry.lab.local:5000/ | Client certificate for mutual TLS |
Trust a CA system-wide (restart Docker after)
sudo cp corp-ca.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates | Debian / Ubuntu |
sudo cp corp-ca.crt /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trust | RHEL / Rocky / Fedora |
sudo cp corp-ca.crt /etc/pki/trust/anchors/ && sudo update-ca-certificates | SUSE / SLES |
Bypass (labs only)
"insecure-registries": ["registry.lab.local:5000"] | daemon.json: skip verification / allow HTTP for this registry, then reload |
podman pull --tls-verify=false registry.lab.local:5000/app:1 | Podman, one command |
skip_verify = true | containerd hosts.toml, per registry host |
Least privilege
docker run --user 10001:10001 app | Run as a non-root UID |
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE app | Drop every capability, add back only what's needed |
docker run --read-only --tmpfs /tmp app | Read-only root filesystem with a writable /tmp |
docker run --security-opt no-new-privileges app | Block privilege escalation through setuid binaries |
docker run --memory 512m --cpus 1 --pids-limit 200 app | Resource limits |
Scan
trivy image shop/api:1.4 | List known CVEs in OS packages and app dependencies |
trivy image --severity HIGH,CRITICAL --exit-code 1 shop/api:1.4 | Fail a CI step on serious findings |
trivy fs --scanners secret . | Look for committed secrets in the source |
Troubleshoot
docker ps -a --format '{{.Names}}\t{{.Status}}' | Exit codes at a glance |
docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}' app | Why it stopped |
docker events --since 30m | What the daemon did recently |
sudo ss -ltnp | grep :8080 | Who owns a port |
docker system df -v | What uses the disk |
docker system prune | Remove stopped containers, unused networks, dangling images and build cache |
docker image prune -a --filter until=168h | Remove unused images older than 7 days |
docker run --rm --network container:app nicolaka/netshoot | Network tools inside another container's namespace |
Build host
docker build -t registry.lab.local:5000/shop/api:1.0 . | Build and name it for the registry in one step |
docker login registry.lab.local:5000 | Log in (credentials go to ~/.docker/config.json) |
docker push registry.lab.local:5000/shop/api:1.0 | Push; note the digest it prints |
Run host
sudo cp domain.crt /etc/docker/certs.d/registry.lab.local:5000/ca.crt | Trust the registry |
sudo dockerd --validate --config-file /etc/docker/daemon.json && sudo systemctl restart docker | Apply host settings |
docker compose -f compose.yaml -f compose.prod.yaml up -d --wait | Start the stack and wait for health |
docker compose ps && curl -s localhost:8080/healthz | Verify |
Helper script
./app.sh up | Build both images and start them with Compose, wait until healthy |
./app.sh run | Same with plain docker: network, volume, two docker run commands |
./app.sh expose --open | Show the URLs for other machines and open the host firewall port |
./app.sh test | Health check, add a quote, list quotes through the front end |
./app.sh down / ./app.sh clean | Stop (keep data) / remove everything |
WEB_PORT=9090 BIND_ADDR=127.0.0.1 ./app.sh up | Another port, reachable only from this machine |
By hand
docker build -t quotes-backend:1.0 ./backend | Build the back end |
docker network create quotes-net | Network with DNS by container name |
docker run -d --name quotes-backend --network quotes-net --network-alias backend -v quotes-data:/data quotes-backend:1.0 | Back end: no -p, reachable only on the network |
docker run -d --name quotes-frontend --network quotes-net -p 8080:8080 quotes-frontend:1.0 | Front end: the only published port |
docker run --rm --network quotes-net curlimages/curl -s http://quotes-frontend:8080/api/quotes | Test from inside the network |
Linux containers
./app.sh up | Build and start both containers with Compose, wait until healthy |
./app.sh run | Same with plain docker run |
./app.sh expose --open | URLs for other machines; open ufw/firewalld |
./app.sh test / ./app.sh clean | Smoke test / remove everything |
Windows containers (PowerShell)
docker info --format '{{.OSType}}' | windows = Windows-containers mode |
& $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon | Switch Docker Desktop between Linux and Windows containers |
.\app.ps1 up | Build from Dockerfile.windows and start with compose.windows.yaml |
.\app.ps1 expose -Open | Add an inbound Windows Firewall rule (as Administrator) |
docker compose -f compose.windows.yaml up -d --build | The same without the helper |
docker run --isolation=hyperv ... | Run an image built for another Windows version in a lightweight VM |
.NET image details
mcr.microsoft.com/dotnet/sdk:10.0 | Build stage: SDK (large, never shipped) |
mcr.microsoft.com/dotnet/aspnet:10.0 | Run stage: ASP.NET Core runtime only |
USER $APP_UID | The non-root 'app' user built into .NET 8+ Linux images |
ASPNETCORE_HTTP_PORTS=8080 | Default listening port in .NET 8+ images |