Docker is the de facto standard for containerization, but knowing the CLI flags is only half the battle. The real skill lies in using those commands to diagnose issues, verify deployments, and recover from failures without causing downtime. This guide moves beyond a simple cheat sheet. It structures essential Docker commands around an operational lifecycle: inventory, configuration, verification, failure recovery, and ongoing operations. Every command includes the context of when to use it, what output to expect, and how to interpret failure signals.
1. Version and Environment Inventory
Before changing anything, you must establish a baseline. This phase is read-only. Its goal is to answer: What is running, where is the data, and what version are we on?
Check Docker Engine and Client Versions
Start by confirming compatibility between client and server. Mismatches often explain cryptic API errors.
docker version --format '{{.Client.Version}} / {{.Server.Version}}'
Expected output: 24.0.7 / 24.0.7 (versions should match). Failure signal: Client version newer than Server (or vice versa) indicates an incomplete upgrade.
List Running Containers with Context
The default docker ps hides critical context. Use a custom format to see names, status, and port mappings immediately.
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"
Why this matters: You identify the exact container name (required for subsequent commands), see if a container is Up 2 minutes (recent restart loop) vs Up 3 weeks, and verify port bindings match your reverse proxy or firewall rules.
Inspect Container Configuration and Mounts
When debugging "it works on my machine" or permission errors, docker inspect is the source of truth. Filter for the specific sections you need rather than parsing massive JSON.
# Check mounts (volumes/binds) and source paths
docker inspect --format '{{json .Mounts}}' <container_name> | jq .
# Check environment variables (redact secrets in output)
docker inspect --format '{{json .Config.Env}}' <container_name> | jq .
# Check networking mode and IP
docker inspect --format '{{.NetworkSettings.IPAddress}} / {{.HostConfig.NetworkMode}}' <container_name>
Practical example: A Python app fails to write to /data. inspect reveals the mount source is /host/path/data but the host directory is owned by root:root while the container runs as uid=1000. The fix is on the host (chown 1000:1000 /host/path/data), not in the container.
Inventory Docker Compose Projects
If you use Compose (v2 plugin), inventory the project state separately. Compose manages networks and volumes implicitly.
# List compose projects and their status
docker compose ls --all
# Deep dive into a specific project
docker compose -p my-project ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
docker compose -p my-project config # Validates and renders the effective config
Data Persistence Verification (The Restart Test)
This is the single most important inventory step for stateful services. Do not assume volumes are configured correctly.
docker stop <container_name>docker rm <container_name>- Redeploy via your standard method (Compose up, run command, CI/CD).
- Verify application data (database records, uploaded files, session persistence) survives.
Failure mode: Data disappears. The container was writing to the writable layer (union filesystem) instead of a defined volume or bind mount. Recovery: Data is likely lost unless you committed the container to an image (docker commit) before removal. Define a named volume in docker-compose.yml or docker run -v and repeat the test.
2. Safe Configuration Path
Changes to running infrastructure should be atomic, reversible, and auditable. Never edit a running container's filesystem directly (docker exec -it ... vi /etc/nginx/nginx.conf). That change vanishes on restart.
Image Management: Pull, Tag, and Verify
Always pull by digest (immutable) in production, tag for human readability.
# Pull specific digest (CI/CD safe)
docker pull nginx@sha256:d85914d547a6c92faa39ce7058bd7529baacab7e0cd4255442b04577c4d1f4e4
# Tag locally for deployment scripts
docker tag nginx@sha256:d859... my-registry.com/prod/nginx:1.25.3
# Verify image layers and history (check for secrets in layers)
docker history my-registry.com/prod/nginx:1.25.3 --no-trunc
Updating a Service: The "Blue/Green" Container Swap
For zero-downtime updates on a single host without an orchestrator (Swarm/K8s), run the new version alongside the old, verify health, then switch the proxy.
- Start new container on a different port (e.g., 8081) with the new image tag.
docker run -d --name app_v2 --network app_net -p 8081:80 my-registry.com/prod/app:v2.1.0
- Verify health (see Section 3).
- Update reverse proxy (Nginx/Traefik/Caddy config) to point upstream to
app_v2:80(internal Docker DNS) orlocalhost:8081. - Reload proxy (
docker exec proxy nginx -s reload). - Monitor error rates for 5 minutes.
- Stop old container (
docker stop app_v1 && docker rm app_v1).
Rollback: Revert proxy config -> reload -> docker start app_v1. Blast radius: seconds.
Managing Secrets and Configs
Never pass secrets via -e ENV_VAR=value (visible in docker inspect and ps -ef on host).
- Docker Secrets (Swarm mode only):
echo "password" | docker secret create db_pass - - Bind-mounted files (Standalone/Compose):
# docker-compose.yml
services:
app:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt # .gitignored file on host
The file appears at /run/secrets/db_password inside the container. Application reads the file path, not the env var.
Resource Constraints (Preventing Noisy Neighbors)
Set limits to prevent one container from OOM-killing the host or starving others.
docker run -d \
--name api \
--memory="512m" --memory-swap="1g" \
--cpus="1.5" \
--pids-limit=100 \
my-image:latest
--memory-swap: Allows bursting to swap (total 1.5GB) but prevents OOM kill if RAM spikes briefly.--pids-limit: Prevents fork bombs.
3. Verification and Diagnostics
You have deployed a change. Now you prove it works. Verification is active; diagnostics are reactive.
Health Checks: The Contract Between Container and Orchestrator
Define a HEALTHCHECK in the Dockerfile or docker-compose.yml. Docker updates the container status to healthy/unhealthy.
# Dockerfile example
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -f http://localhost:8080/healthz || exit 1
Observe health status:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Health}}"
Status progression: starting -> healthy (good) OR starting -> unhealthy (investigate logs immediately).
Log Aggregation and Filtering
docker logs is your first line of defense. Use flags to avoid flooding the terminal.
# Follow last 200 lines with timestamps (essential for correlation)
docker logs -f --tail 200 -t <container_name>
# Filter for errors (case insensitive) since a specific time
docker logs --since "2024-01-15T10:00:00" <container_name> 2>&1 | grep -i -e error -e exception -e fatal
# Export to file for offline analysis / vendor support
docker logs <container_name> > /tmp/container_logs_$(date +%F).txt 2>&1
Note on logging drivers: If using json-file (default), logs live on host disk (/var/lib/docker/containers/<id>/<id>-json.log). Rotate them via /etc/docker/daemon.json ("log-opts": {"max-size": "10m", "max-file": "3"}) to prevent disk pressure.
Network Diagnostics Inside the Container
"Connection refused" inside a container usually means: wrong port, service not listening on 0.0.0.0, or network policy.
# Enter container network namespace (best toolkit)
docker exec -it <container_name> sh -c "apk add --no-cache curl bind-tools netcat-openbsd iproute2 2>/dev/null || apt-get update && apt-get install -y curl dnsutils netcat iproute2 2>/dev/null"
# Test internal connectivity (DNS resolves service names in user-defined networks)
docker exec <container_name> curl -v http://database:5432 # TCP check
docker exec <container_name> nslookup database # DNS check
docker exec <container_name> ss -tulpn # List listening ports INSIDE container
Common finding: App listens on 127.0.0.1:8080 inside container. External requests (even from same host via port mapping) fail. Fix: Bind to 0.0.0.0:8080.
Resource Usage Monitoring (cgroups v1/v2)
Spot memory leaks or CPU throttling before the OOM killer strikes.
# Real-time stream (like top)
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}"
# Check for OOM kills in kernel log (host level)
dmesg -T | grep -i -e oom -e kill
4. Failure Modes and Recovery
When verification fails, you need a runbook, not panic. These are the most common Docker-specific failure patterns and their recoveries.
1. Container Exits Immediately (CrashLoopBackOff Equivalent)
Symptom: docker ps -a shows Exited (137) 2 minutes ago or Restarting (1) 5s ago. Diagnosis:
docker logs --tail 50 <container_name>
docker inspect --format '{{.State.ExitCode}} {{.State.Error}} {{.State.OOMKilled}}' <container_name>
- Exit Code 137 (SIGKILL): Almost always OOM (Out Of Memory). Check
docker statshistory; increase--memorylimit or fix application leak. - Exit Code 1/127: Application error (missing binary, config syntax error, migration failure). Read logs.
- Exit Code 0: Process finished successfully (batch job) or entrypoint script finished without
exec-ing the main process. Fix: Ensure entrypoint ends withexec "$@".
2. Port Conflicts / "Address Already in Use"
Symptom: docker run fails: Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use. Diagnosis:
# Check what holds port 80 on HOST
sudo ss -tulpn | grep :80
# Or check if another container owns it
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep :80
Recovery:
- Stop conflicting host process (systemd service?).
- Change container port mapping:
-p 8080:80. - Bind to specific IP:
-p 127.0.0.1:80:80(localhost only).
3. Permission Denied on Volumes
Symptom: App logs PermissionError: [Errno 13] Permission denied: '/data'. Diagnosis:
# Host side
ls -ld /host/path/data
# Container side
docker exec <container_name> ls -ld /data
docker exec <container_name> id # Check UID/GID running app
Recovery:
- Named Volume (Preferred):
docker volume create app_data. Docker handles permissions (usually root inside container, but app runs as non-root? Useuser: "1000:1000"in compose orDockerfile USER). - Bind Mount:
chown -R 1000:1000 /host/path/dataon host (match container UID). Or use--userns-remap(advanced).
4. Image Pull Failures (Registry Auth / Rate Limits)
Symptom: Error response from daemon: pull access denied or toomanyrequests. Recovery:
- Auth:
docker login my-registry.com(stores creds in~/.docker/config.json). For CI:echo $TOKEN | docker login -u $USER --password-stdin. - Rate Limits (Docker Hub): Use a mirror/proxy (e.g.,
registry.docker.iomirror in/etc/docker/daemon.json), authenticate (higher limits), or vendor images to private registry.
5. Disk Pressure: "No Space Left on Device"
Symptom: docker run fails, builds fail, host sluggish. Diagnosis:
df -h /var/lib/docker
docker system df -v # Detailed breakdown: images, containers, volumes, build cache
Recovery (Safe Pruning):
# 1. Remove stopped containers (safe)
docker container prune -f
# 2. Remove dangling images (untagged, no container ref) - SAFE
docker image prune -f
# 3. Remove unused volumes (DANGEROUS - DATA LOSS) - ONLY if you know they are orphaned
# docker volume prune -f # DO NOT RUN BLINDLY
# 4. Remove build cache (safe, slows next build)
docker builder prune -f
Prevention: Set up log rotation (daemon.json), monitor docker system df via Prometheus/node_exporter, schedule weekly prune cron for safe items only.
5. Operations Checklist
Integrate these into your daily/weekly routines and CI/CD pipelines.
Daily / Per-Deployment
- [ ]
docker compose configvalidates syntax and renders final config beforeup. - [ ]
docker pull <image@digest>succeeds in CI before deploy stage. - [ ] Health check passes (
healthystatus) for 2 consecutive intervals post-deploy. - [ ]
docker logs --since <deploy_time> <container> | grep -i errorreturns zero critical lines. - [ ] Reverse proxy / Load Balancer shows backend as
UP.
Weekly
- [ ] Run
docker system df -v; alert if reclaimable space > 20GB or > 50% total. - [ ] Verify backup restoration for one named volume (test restore to staging).
- [ ] Scan images for CVEs:
docker scout cves <image>ortrivy image <image>. Block deploy on Critical/High if policy dictates. - [ ] Review
docker ps -afor "zombie" containers (Exited > 7 days). Remove after confirming not needed for forensics.
Monthly / Quarterly
- [ ] Full Disaster Recovery Drill: Spin up stack from scratch on clean host using only Git repo (Compose files, scripts,
.env.example) and backed-up volumes. Time it. Document gaps. - [ ] Audit
docker imagesfor base image freshness. Rebuild dependent images on updated base (e.g.,python:3.11-slim->python:3.12-slim). - [ ] Review Docker daemon config (
/etc/docker/daemon.json): log rotation, live-restore, userns-remap, insecure-registries (remove if unused). - [ ] Rotate registry credentials and update CI/CD secrets.
Conclusion
Docker commands are only as reliable as the operational discipline surrounding them. Memorizing flags for docker run or docker compose up is the entry fee; the practice is building a workflow where every change is observed, verified, and reversible. Start by instrumenting your current stack: add health checks to every service, enforce named volumes for state, and script the restart test into your CI pipeline. When the next 3 AM alert fires, you will not be reading docker --help; you will be running docker logs --tail 200 -t <container>, seeing the error, executing the documented rollback, and going back to sleep. That is the standard this guide prepares you for.