E-NO
Docker CLI architecture 10 Min Read

Docker CLI Architecture Explained with Practical Examples

calendar_today Published: 2026-09-12
update Last Updated: 2026-09-12
analytics SEO Efficiency: 100%
Technical guide illustration for Docker CLI Architecture Explained with Practical Examples.

Intro

Docker CLI architecture explained with practical examples helps operators move from an observed problem to a verified result. Start by identifying the installed version, deployment topology, prerequisites, and the exact component being inspected.

This article focuses on Docker CLI architecture for developers, DevOps consultants, and technical startup teams. It connects Docker CLI components, Docker CLI data flow, Docker CLI design, and Docker CLI operations to commands, expected output, failure signals, and recovery decisions that match the selected technology.

The goal is operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document how to recover if the expected state is not reached.

Version and Environment Inventory

Before troubleshooting or changing any Docker setup, establish a clear picture of the environment. Start with the installed Docker version and the server/client split. Run docker version to see both the client and server details, including the API version and OS/Arch. A typical output looks like this:

Client: Docker Engine - Community
 Version:           24.0.5
 API version:       1.43
 Go version:        go1.20.6
 Git commit:        ced0996
 Built:             Fri Jul 21 12:50:07 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.5
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.6
  Git commit:       a61e2b4
  Built:            Fri Jul 21 12:50:07 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

Also check docker info for storage driver, logging driver, cgroup version, and kernel details. This tells you whether the daemon uses overlay2, what the default logging driver is, and whether there are any warnings about unsupported configurations. For example, if you see WARNING: No swap limit support, you know that memory constraints may not be enforced as expected.

Next, list all containers with docker ps -a to see both running and stopped containers. Use a custom format to keep the output readable:

docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"

This shows the container name, image, current state (e.g., Up 2 hours, Exited (1) 5 minutes ago), and port mappings. A stopped container with a non-zero exit code often indicates a problem.

For Compose projects, run docker compose ps to see the status of all services in the current project. If you need to inspect the configuration that Compose would use, run docker compose config to render the effective Compose file, including merged environment variables and default values.

When data persistence is involved, confirm where files are stored before changing containers. A named volume such as app_data:/var/lib/app is managed by Docker and is usually easier to reuse across container rebuilds. A bind mount such as ./data:/var/lib/app maps a host directory directly and is useful for local development, but it can expose permission, portability, and backup problems if the same path does not exist on another machine. Use docker inspect <container> to check the mounts:

docker inspect --format '{{ json .Mounts }}' <container>

This returns a JSON array of mounts, including type, source, destination, and options. For example:

[
  {
    "Type": "volume",
    "Name": "app_data",
    "Source": "/var/lib/docker/volumes/app_data/_data",
    "Destination": "/var/lib/app",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

As part of the environment inventory, a small production-like local test should include a restart test: stop the container, recreate it, and confirm the application still sees the expected files. If the data disappears, the service was probably writing to the container filesystem instead of a volume or mount.

Quick check 1 of 2

When inspecting a stopped container, you see "Exited (1) 5 minutes ago" in the docker ps -a output. According to the failure modes section, what does a non-zero exit code typically indicate?

The article states that a stopped container with a non-zero exit code often indicates a problem.

Safe Configuration Path

Safe configuration changes in Docker start with understanding the current configuration and the scope of any change. The first rule is to separate observation from intervention. Capture current state and timestamps first, protect credentials and private material, then change one scoped item only when its blast radius and recovery path are understood.

For configuration, Docker CLI offers several ways to inspect and modify settings. Use docker inspect to see the full configuration of a container, image, network, volume, or other object. The output can be large, so filter it with --format and Go templates. For example, to get just the environment variables of a container:

docker inspect --format '{{ range .Config.Env }}{{ println . }}{{ end }}' <container>

This prints each environment variable on its own line. To see the restart policy:

docker inspect --format '{{ .HostConfig.RestartPolicy.Name }}' <container>

If you need to change a container's configuration, such as environment variables or resource limits, you usually need to recreate the container. With docker run, you must stop, remove, and run again with the new flags. With Compose, you can edit the docker-compose.yml and run docker compose up -d to recreate only the affected services. Compose handles the diff and recreates containers whose configuration changed.

Here is an example of a safe change workflow for a Compose service that needs a new environment variable:

  1. Record the current state: docker compose ps and docker compose logs --tail 50 <service>.
  2. Back up the existing Compose file: cp docker-compose.yml docker-compose.yml.bak-20230721.
  3. Edit the file to add the environment variable under environment: for the service.
  4. Validate the configuration: docker compose config to ensure syntax and merged values are correct.
  5. Apply the change: docker compose up -d <service>.
  6. Verify the new container is running: docker compose ps and inspect the environment with docker inspect --format '{{ .Config.Env }}' <container>.
  7. If something goes wrong, roll back by restoring the backup and running docker compose up -d again.

For production-like environments, always test configuration changes in a staging environment first. Use environment-specific Compose override files (e.g., docker-compose.prod.yml) to avoid accidentally applying development settings to production.

When handling secrets, never pass them directly in the command line or hard-code them in Compose files. Instead, use Docker secrets (in Swarm) or external secret management tools, or at least use environment variables with a placeholder and inject the real value at runtime. For example, in Compose you can use ${DB_PASSWORD} and set the actual value in a .env file that is not committed to version control.

Verification and Diagnostics

Verification and diagnostics require a systematic approach: observe, hypothesize, test, and confirm. Docker provides several commands for this. Start with docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" to see what is running and their status. Then use docker logs <container> --tail 100 to read recent output. Add -f to follow logs in real time, or --since 10m to see logs from the last 10 minutes. For containers that log to stderr, use docker logs <container> 2>&1 | tail -100 to combine streams.

If a container is running but the application is unresponsive, inspect the container's health status if a healthcheck is defined. Run docker inspect --format '{{ .State.Health.Status }}' <container> to get the health state (starting, healthy, unhealthy). To see the healthcheck logs, use docker inspect --format '{{ json .State.Health.Log }}' <container> and parse the output.

For deeper inspection, use docker exec -it <container> sh to get a shell inside the container (if the image has a shell). From there, you can check processes with ps aux, network connections with netstat -tulpn or ss -tulpn, and file contents. This is especially useful for debugging without changing the image.

For Compose projects, use docker compose ps to see service status, docker compose logs -f <service> to follow logs, and docker compose exec <service> sh to run commands inside a service container. If you need to test a network connection between containers, use docker compose exec <service> ping <other_service> or nc -zv <other_service> <port> if netcat is available.

Diagnostics also include monitoring resource usage. docker stats shows CPU, memory, network I/O, and block I/O for all running containers. Use docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" for a one-time snapshot. This helps identify containers that are consuming too many resources.

When troubleshooting a specific issue, follow this sequence:

  1. Reproduce the problem and capture the exact error message.
  2. Check the container status and exit code: docker ps -a and docker inspect --format '{{ .State.ExitCode }}' <container>.
  3. Read logs: docker logs <container> with appropriate flags.
  4. Check the container's configuration: docker inspect <container>.
  5. Test connectivity: from inside the container or from the host to the container's ports.
  6. If needed, use strace or similar tools inside the container to trace system calls.

Document each finding and the commands used, so that the diagnosis is reproducible.

Failure Modes and Recovery

Docker systems fail in predictable ways. Knowing these failure modes and how to recover is crucial for minimal downtime.

Container exits immediately after start

This often indicates a misconfigured command, missing environment variable, or an application error. Check the exit code with docker inspect --format '{{ .State.ExitCode }}' <container> and the logs with docker logs <container>. Common exit codes: 1 general error, 126 command not executable, 127 command not found. For example, if the container tries to run a script that is not executable, you might see exit code 126. Recovery: fix the Dockerfile or command, rebuild the image, and restart.

Container runs but application is unreachable

This could be due to port mapping errors, network misconfiguration, or the application listening on the wrong interface. Check port mappings with docker port <container> and the container's network settings with docker inspect --format '{{ json .NetworkSettings.Ports }}' <container>. Ensure the application inside the container listens on 0.0.0.0, not just 127.0.0.1, because the container's localhost is different from the host's. Recovery: adjust the application configuration or the Docker run command to correctly map ports.

Volume data not persisting

If data disappears after container recreation, the container was likely writing to its writable layer rather than a volume or bind mount. Verify mounts with docker inspect --format '{{ json .Mounts }}' <container>. Recovery: update the Dockerfile or Compose file to use a named volume or bind mount, and if necessary, copy data out of the old container using docker cp before recreating.

Daemon not responding

If docker ps hangs or returns an error like Cannot connect to the Docker daemon at unix:///var/run/docker.sock, the daemon may be down. Check the daemon status with systemctl status docker (on systemd systems) or service docker status. Look at the daemon logs with journalctl -u docker or /var/log/docker.log. Recovery: restart the daemon with systemctl restart docker and investigate the cause.

Image pull failures

Images may fail to pull due to network issues, registry authentication, or missing tags. Check docker pull output for specific errors. For private registries, ensure you are logged in with docker login <registry> and that credentials are valid. For rate limits on Docker Hub, consider using a mirror or increasing the pull limit by authenticating. Recovery: resolve network or auth issues and retry.

Resource exhaustion

Containers may be killed due to out-of-memory (OOM) or CPU limits. Check docker inspect --format '{{ .State.OOMKilled }}' <container> to see if OOM killed it. Check docker events for OOM events. Recovery: increase memory limits with --memory and --memory-swap flags, or optimize the application.

For all failure modes, have a rollback plan. For stateless services, rolling back to a previous image version is easy. For stateful services, ensure backups are working before any change.

Quick check 2 of 2

In the failure modes guidance, what does exit code 127 specifically mean?

The article lists common exit codes: 1 general error, 126 command not executable, 127 command not found.

Operations Checklist

Use this checklist before and after any Docker operation to ensure consistency and safety.

Before making changes:

  1. Record the current state: docker ps -a, docker images, docker network ls, docker volume ls.
  2. Check the daemon health: docker info and look for warnings.
  3. Back up any critical configuration files (Compose files, daemon.json).
  4. If modifying data containers, back up volumes: docker run --rm -v <volume>:/data -v $(pwd):/backup alpine tar czf /backup/volume-backup.tar.gz -C /data .
  5. Determine the smallest change that achieves the goal.
  6. Define the expected outcome and how to verify it.
  7. Document the rollback procedure.

After making changes:

  1. Verify the change took effect: e.g., docker inspect or docker ps.
  2. Run functional tests: e.g., curl the service endpoint.
  3. Check logs for errors: docker logs --tail 100 <container>.
  4. Monitor resource usage for a few minutes: docker stats --no-stream.
  5. Update documentation with the new state.
  6. If something fails, execute the rollback procedure and analyze the cause.

Ownership and review frequency: For each major configuration change, assign a single accountable owner (e.g., DevOps engineer or team lead) who approves the change and ensures the checklist is followed. Review this checklist itself monthly to incorporate lessons learned.

Example: When updating a production web service with Compose, owner: "Priya Shah, Engineering Lead". She approves the new image tag after staging tests. The change is applied during a maintenance window, and the team revisits the rollout results in the next weekly incident review.

Common Pitfalls and How to Avoid Them

Running containers with --privileged

--privileged grants the container almost all host capabilities, which is dangerous. It is often used unnecessarily for tasks like accessing USB devices or debugging. Instead, use --cap-add to add only the needed capabilities, or use devices with --device. Avoid --privileged in production.

Using latest tag in production

The latest tag is a moving target and can lead to unpredictable deployments. Always pin to a specific version or digest (e.g., myapp:1.2.3 or myapp@sha256:...). This ensures reproducibility and easier rollback.

Hardcoding secrets in Dockerfiles or Compose files

Secrets in images can be extracted even after being deleted in a layer. Use environment variables at runtime, Docker secrets, or external secret managers. Never commit secrets to version control.

Not setting resource limits

Without --memory and --cpus, a container can consume all host resources and starve other processes. Always set limits in production, and consider setting default limits in the daemon configuration.

Ignoring container exit codes

A container that exits with a non-zero code is a signal that something went wrong. Always check the exit code and logs, and set restart policies appropriately (--restart=unless-stopped for most services) but avoid restart loops that hide problems.

Overusing bind mounts in production

Bind mounts depend on the host filesystem and can cause permission issues and portability problems across environments. Prefer named volumes for persistent data. If bind mounts are necessary, document the host path requirements and ensure consistent permissions.

Not using healthchecks

Without a healthcheck, Docker cannot determine if a container is actually healthy, only if it is running. Define a healthcheck in the Dockerfile or Compose file to allow orchestration tools to react to unhealthy containers.

Accumulating unused images and volumes

Over time, unused images and volumes consume disk space. Use docker system prune carefully to remove unused objects. Schedule regular cleanup with filters: docker image prune -a --filter "until=168h" to remove images older than a week.

Conclusion

Docker CLI architecture explained with practical examples becomes truly useful when each recommendation is version-scoped, observable, and reversible where the technology permits. Copying a command without checking prerequisites and expected output is not an operations procedure; it is a gamble.

As a next step, choose one low-risk verification from this article, record the current state, run the documented check, compare the result with the expected signal, and review dependencies such as Docker Context, Docker Images, and Docker Containers.

A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision. By following the structured approach outlined here, you can operate Docker with greater confidence and reduce the risk of unexpected downtime.

Related Research

Article Quality Score

Reader usefulness 100%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL