Docker for cloud application delivery
Docker packages an application and its runtime dependencies into an image that can be tested, distributed, and started consistently. That consistency is valuable, but a container is not automatically secure, observable, or production-ready. The quality of the image, runtime configuration, deployment process, and operational controls still matters.
This guide explains Docker from first principles through production operations. It focuses on repeatable builds, small images, explicit configuration, least privilege, reliable health checks, secure registry workflows, and evidence-led troubleshooting.
Who this Docker guide is for
Use this guide if you are:
- learning the relationship between Docker images and containers;
- creating or reviewing a Dockerfile;
- running a multi-container application with Docker Compose;
- publishing images to a private registry such as Azure Container Registry;
- adding container builds to CI/CD;
- improving container security and reliability; or
- investigating a build, startup, networking, storage, or registry problem.
You should finish with a clear mental model, a practical command workflow, and a production review checklist.
Docker in one mental model
Docker uses a client-server architecture:
- The Docker client accepts commands such as
docker buildanddocker run. - The Docker daemon builds images and manages containers, networks, and volumes.
- A registry stores and distributes images.
- An image is an immutable package assembled from filesystem layers and metadata.
- A container is a running or stopped instance of an image with a writable container layer.
An image is the deployable artifact. A container is a process created from that artifact. Deleting a container does not delete its image, and rebuilding an image does not update containers that are already running.
Containers are not virtual machines
A virtual machine includes a guest operating system and virtualized hardware. A container normally shares the host kernel while isolating processes, filesystems, networking, and resource access.
This distinction has practical consequences:
- containers start quickly because they do not boot a guest operating system;
- the image must be compatible with the host kernel and CPU architecture;
- a container boundary is useful isolation, but it is not a reason to ignore host security;
- processes inside the container should still run with minimum privilege; and
- durable data should live outside the writable container layer.
Verify the Docker environment
After installing Docker Desktop or Docker Engine from the official instructions for your platform, confirm the client and daemon are available:
docker version
docker info
docker context show
docker version shows client and server details. If only the client section appears, the CLI cannot reach the daemon. docker info provides storage, runtime, security, resource, and registry information. docker context show confirms which Docker endpoint the client is targeting.
Run a small verification container:
docker run --rm hello-world
The --rm option removes the stopped container automatically. It does not delete the downloaded image.
Your first container workflow
Start an NGINX container and publish it on local port 8080:
docker run --detach \
--name cloudforge-web \
--publish 8080:80 \
nginx:alpine
Inspect it:
docker ps
docker logs cloudforge-web
docker inspect cloudforge-web
curl --fail http://localhost:8080
Stop and remove it:
docker stop cloudforge-web
docker rm cloudforge-web
The port mapping is host-port:container-port. Publishing 8080:80 makes port 80 inside the container reachable on port 8080 of the Docker host.
Understand the container lifecycle
A container commonly moves through these states:
| State | Meaning | Useful command |
|---|---|---|
| Created | Defined but not started | docker start <name> |
| Running | Main process is active | docker ps |
| Paused | Processes are suspended | docker unpause <name> |
| Exited | Main process ended | docker ps --all |
| Restarting | Restart policy is starting it again | docker logs <name> |
| Dead | Docker could not remove or restart it cleanly | inspect the daemon and host |
The main process determines container lifetime. If PID 1 exits, the container stops. Do not keep a container alive with an unrelated infinite loop; fix the application command and foreground behavior instead.
Essential inspection commands
docker ps --all
docker logs --timestamps --tail 200 <container>
docker inspect <container>
docker stats --no-stream <container>
docker top <container>
docker port <container>
docker diff <container>
Use docker exec only when the container is running:
docker exec --interactive --tty <container> sh
Minimal images may not include Bash, curl, package managers, or diagnostic tools. That is expected. Prefer logs, health endpoints, metrics, docker inspect, or a purpose-built debug container instead of permanently expanding a production image.
Images, tags, and digests
An image reference commonly looks like this:
registry.example.com/platform/orders-api:1.8.3
It contains a registry, repository, and tag. Tags are human-friendly pointers and can be moved. A digest identifies immutable image content:
registry.example.com/platform/orders-api@sha256:<digest>
For reproducible releases:
- use an intentional version tag instead of relying on
latest; - record the image digest produced by the build;
- promote the same tested image between environments;
- avoid rebuilding separately for test and production; and
- pin critical base images by digest when your update process can maintain them.
List and inspect local images:
docker image ls
docker image inspect <image>
docker history --no-trunc <image>
How image layers work
Most Dockerfile instructions create a layer. Docker can reuse unchanged layers from its build cache. Layer order therefore affects build speed and cache effectiveness.
Copy dependency manifests before application source when dependencies change less often:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
If source code were copied first, every source change could invalidate the dependency-installation layer.
Layers are content-addressed and shared where possible. Deleting a sensitive file in a later layer does not guarantee it is absent from earlier layers. Never copy secrets into a build context or image.
Build context and .dockerignore
The final argument to docker build is the build context:
docker build --tag cloudforge-api:local .
The period sends the current directory as context. Keep the context narrow and create a .dockerignore file:
.git
.env
.env.*
node_modules
.next
dist
coverage
*.log
README.md
Dockerfile*
compose*.yaml
Adjust the exclusions to the application. Do not exclude a file the build genuinely needs. A smaller context improves transfer time, cache stability, and the chance of keeping credentials out of the image.
Dockerfile instructions that matter
| Instruction | Purpose | Production note |
|---|---|---|
FROM | Selects a base image | use trusted, maintained images |
WORKDIR | Sets the working directory | prefer it over repeated cd commands |
COPY | Copies files from build context | use explicit paths and ownership |
RUN | Executes a build step | combine related package operations |
ARG | Supplies build-time values | not suitable for secrets |
ENV | Sets runtime environment defaults | values remain visible in image metadata |
EXPOSE | Documents a listening port | does not publish the port |
USER | Selects the runtime user | use a non-root identity |
HEALTHCHECK | Defines container health evaluation | keep checks lightweight and meaningful |
ENTRYPOINT | Defines the executable | useful for a stable application command |
CMD | Defines default arguments or command | use JSON form for signal handling |
Prefer exec-form commands:
CMD ["node", "dist/server.js"]
The exec form avoids an unnecessary shell process and generally gives the application clearer access to termination signals.
A production-oriented Node.js Dockerfile
This example separates dependency installation, compilation, and runtime content:
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM dependencies AS build
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM node:24-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json ./package.json
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Adapt the runtime files and command to the framework. Before using this pattern:
- confirm
npm run buildproducesdist; - confirm all runtime packages remain after
npm prune --omit=dev; - pin or deliberately update the base image;
- add an application health endpoint;
- scan the resulting image; and
- test shutdown behavior with
docker stop.
Build and run it:
docker build --pull --tag cloudforge-api:local .
docker run --rm \
--name cloudforge-api \
--publish 3000:3000 \
--env-file .env.local \
cloudforge-api:local
Do not commit .env.local. Environment variables are convenient configuration, but they are not automatically a secure secret-management system.
Multi-stage builds
Multi-stage builds use multiple FROM instructions. Build tools remain in earlier stages while the final stage receives only the files required at runtime.
Benefits include:
- smaller runtime images;
- fewer packages and a smaller attack surface;
- a clearer separation between compilation and execution; and
- easier reproducibility between CI and local builds.
Target a particular stage while debugging:
docker build --target build --tag cloudforge-api:build-debug .
Do not assume a smaller image is automatically safer. Package provenance, vulnerability status, runtime permissions, update cadence, and configuration all remain important.
Build cache and BuildKit
Docker BuildKit improves concurrent execution, cache handling, and advanced build mounts. A package-manager cache can speed repeated builds without entering the final image:
RUN --mount=type=cache,target=/root/.npm npm ci
If a private package feed is required, use a BuildKit secret mount rather than ARG, ENV, or a copied credentials file:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Build with the secret:
docker build \
--secret id=npmrc,src="$DOCKER_NPMRC_PATH" \
--tag cloudforge-api:local .
The secret source should be supplied by an approved local or CI secret store. Never print it in build logs.
Runtime configuration and secrets
Separate configuration from the image so one image can be promoted through environments.
Common configuration sources include:
- environment variables;
- mounted configuration files;
- orchestrator-managed secrets;
- a managed secret store retrieved through workload identity; and
- command-line arguments for non-sensitive options.
Avoid these patterns:
- hard-coded credentials in a Dockerfile;
- secrets passed through build arguments;
- credentials copied into an image and deleted in a later layer;
- tokens embedded in an image tag or registry URL; and
- secrets committed in Compose files.
Inspect the effective runtime configuration without exposing secret values in shared logs. Treat docker inspect output as potentially sensitive because it can include environment variables.
Container storage
The writable container layer is temporary and coupled to that container. Use external storage for durable data.
Named volumes
Docker manages named volumes:
docker volume create cloudforge-data
docker run --rm \
--mount source=cloudforge-data,target=/var/lib/example \
example-image
Named volumes are appropriate for persistent application data on a Docker host. They still require backup, restore testing, access control, capacity monitoring, and lifecycle ownership.
Bind mounts
Bind mounts expose a host path:
docker run --rm \
--mount type=bind,source="$PWD/config",target=/app/config,readonly \
example-image
Bind mounts are useful for local development and explicit host integration, but they reduce portability and expose host filesystem paths. Use read-only mounts when writes are unnecessary.
Temporary filesystems
A tmpfs mount stores non-persistent data in host memory on supported platforms:
docker run --rm \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
example-image
Use it only after checking memory behavior and application requirements.
Docker networking
Containers on a user-defined bridge network can resolve each other by container name or network alias.
docker network create cloudforge-net
docker run --detach \
--name cloudforge-db \
--network cloudforge-net \
postgres:17
docker run --detach \
--name cloudforge-api \
--network cloudforge-net \
--publish 127.0.0.1:3000:3000 \
cloudforge-api:local
Inside the API container, localhost means that API container—not the database and not the host. Use the database service name, such as cloudforge-db, for container-to-container communication.
Publishing to 127.0.0.1 restricts the host listener to loopback in typical local setups. Publishing without a host address may expose the port on all host interfaces, depending on platform and firewall configuration.
Networking investigation commands
docker network ls
docker network inspect <network>
docker inspect --format '{{json .NetworkSettings.Networks}}' <container>
docker port <container>
Check DNS resolution, the application listen address, published ports, host firewalls, proxies, TLS configuration, and upstream health as separate boundaries.
Docker Compose
Docker Compose defines related services, networks, volumes, and configuration in YAML. It is especially useful for local development, integration tests, demonstrations, and controlled single-host deployments.
Create compose.yaml:
services:
api:
build:
context: .
image: cloudforge-api:local
environment:
NODE_ENV: production
DATABASE_HOST: database
DATABASE_PORT: "5432"
DATABASE_NAME: app
DATABASE_USER: app
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD}
ports:
- "127.0.0.1:3000:3000"
depends_on:
database:
condition: service_healthy
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
database:
image: postgres:17
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD}
volumes:
- database-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
database-data:
This example demonstrates structure, not a universal production template. Review secret delivery, image pinning, backup, encryption, network exposure, resource limits, observability, and high availability for the target environment.
Compose command workflow
Validate and view the resolved model:
docker compose config
Start services:
docker compose up --detach --build
Inspect behavior:
docker compose ps
docker compose logs --follow --tail 200
docker compose exec api sh
docker compose top
Stop and remove containers and the default network:
docker compose down
Do not add --volumes unless you intentionally want to remove named Compose volumes and understand the data impact.
Health checks and readiness
A Docker health check reports starting, healthy, or unhealthy. It does not automatically make every deployment platform route traffic correctly; the platform may have its own readiness and liveness model.
An application health endpoint should:
- return quickly;
- avoid changing state;
- test only dependencies required for the intended health signal;
- have strict timeouts; and
- avoid leaking internal or sensitive information.
Example Dockerfile health check when the image contains an appropriate client:
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
Inspect health evidence:
docker inspect --format '{{json .State.Health}}' <container>
Signals and graceful shutdown
docker stop sends a termination signal and waits before forcing the process to exit. The application should:
- receive the signal as PID 1;
- stop accepting new work;
- complete or safely abandon in-flight work;
- close database and message connections; and
- exit within the configured grace period.
Test this behavior rather than assuming it:
time docker stop --time 30 <container>
docker inspect --format '{{.State.ExitCode}}' <container>
Shell wrappers that do not forward signals can break graceful shutdown. Prefer exec-form ENTRYPOINT and CMD, or ensure an entrypoint script uses exec for the application process.
Resource limits and restart behavior
Without limits, a container may compete for all resources available to the Docker host.
Example local controls:
docker run --detach \
--name cloudforge-api \
--memory 512m \
--cpus 1.0 \
--restart unless-stopped \
cloudforge-api:local
Choose limits from measurement. Limits that are too low cause throttling or out-of-memory termination; limits that are absent allow noisy-neighbor failures.
Restart policies can recover a process after some failures, but they can also create a crash loop. Always inspect exit codes and logs instead of treating repeated restarts as recovery.
Logging and observability
Containers should normally write application logs to standard output and standard error. Docker captures those streams through its configured logging driver.
Good application logs include:
- UTC timestamps;
- severity;
- request or correlation identifiers;
- stable event names;
- useful error context; and
- no access tokens, passwords, connection strings, or personal data.
Check the Docker logging configuration and prevent unbounded local log growth. Production environments normally forward logs, metrics, traces, and container events to a centralized platform.
Useful commands:
docker logs --since 30m --timestamps <container>
docker events --since 30m
docker stats
Private registries and Azure Container Registry
A registry workflow typically includes authentication, tagging, pushing, scanning, and deployment by digest.
For Azure Container Registry, authenticate with your approved Azure identity:
az login
az account show --output table
az acr login --name <registry-name>
Tag and push an image:
docker tag cloudforge-api:local \
<registry-name>.azurecr.io/cloudforge/api:1.0.0
docker push <registry-name>.azurecr.io/cloudforge/api:1.0.0
Inspect the pushed manifest:
docker buildx imagetools inspect \
<registry-name>.azurecr.io/cloudforge/api:1.0.0
For automated workloads, use a scoped workload identity or service principal according to the Azure service and organizational controls. Avoid personal credentials and broad administrative roles in CI/CD.
Multi-platform images
An image built for one CPU architecture may not run on another. An exec format error often indicates an architecture mismatch.
Inspect an image:
docker image inspect \
--format '{{.Os}}/{{.Architecture}}' \
cloudforge-api:local
Build a multi-platform image with Buildx and publish it to a registry:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag <registry>/cloudforge/api:1.0.0 \
--push .
Run integration tests for each supported architecture. Emulation can help build or test images, but it may be slower and may not reveal every native-platform issue.
Docker in CI/CD
A dependable container pipeline usually follows this sequence:
- Check out a specific revision.
- Restore only controlled caches.
- Run linting, unit tests, and dependency checks.
- Build the image once from a reviewed Dockerfile.
- Test the built image, including startup and health behavior.
- Scan dependencies and operating system packages.
- Generate and retain build metadata or a software bill of materials where required.
- Sign or attest the artifact if the supply-chain policy requires it.
- Push an immutable release tag and record its digest.
- Deploy the same digest to later environments.
- Verify health, logs, metrics, and rollback capability.
Do not inject environment-specific application content by rebuilding the image at every stage. Promote a verified artifact and supply environment configuration at deployment time.
Container security baseline
Container security spans source, build, registry, host, runtime, network, and application controls.
Image controls
- Start from a trusted, maintained base image.
- Use the smallest practical runtime image, not the smallest image at any cost.
- Remove compilers and package managers from the final stage when unnecessary.
- Rebuild regularly to receive base-image security updates.
- Scan images and triage findings using reachability, severity, exposure, and available fixes.
- Pin deployment artifacts by digest.
- Keep secrets and private keys out of every layer.
Runtime controls
- Run as a non-root user.
- Use a read-only root filesystem when the application supports it.
- Mount only the paths the application needs.
- Drop Linux capabilities that are not required.
- Enable
no-new-privilegeswhere supported. - Apply memory, CPU, and process limits.
- Keep the default seccomp profile unless a reviewed requirement says otherwise.
- Never use
--privilegedas a routine fix.
Example hardened starting point:
docker run --rm \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop ALL \
--security-opt no-new-privileges \
--memory 512m \
--cpus 1.0 \
cloudforge-api:local
Applications may require a specific capability or writable directory. Add only the reviewed exception and document why it exists.
Protect the Docker socket
Access to the Docker daemon is highly privileged. A user or container that can control the daemon can often gain extensive control over the host.
- Do not mount
/var/run/docker.sockinto ordinary application containers. - Limit membership in groups that can access the daemon.
- Protect remote daemon endpoints with approved authentication and network controls.
- Consider rootless mode where it fits the platform and workload.
- Audit automation that creates privileged containers or host mounts.
Production deployment choices
Docker Compose can be appropriate for controlled single-host workloads, but it does not by itself provide a complete multi-host scheduler, managed control plane, or every high-availability capability.
Choose a deployment target based on:
- availability and scaling requirements;
- team operating capability;
- networking and identity needs;
- compliance boundaries;
- deployment frequency;
- stateful workload requirements; and
- observability and incident-response expectations.
On Azure, possible targets include Azure Container Apps, Azure App Service for Containers, Azure Kubernetes Service, Azure Container Instances, and virtual machines running a container engine. The right target depends on the workload rather than Docker alone.
A systematic troubleshooting workflow
Use evidence in this order:
- Confirm the exact command, host, context, image reference, and timestamp.
- Determine whether the failure occurs during build, pull, create, start, health evaluation, or request handling.
- Capture container state, exit code, error, OOM status, and restart count.
- Read application and daemon logs around the same timestamp.
- Inspect mounts, environment, networks, published ports, user, and resource limits.
- Reproduce with the smallest safe input.
- Change one variable and compare the result.
- Verify recovery and record the underlying cause.
Start with:
docker context show
docker version
docker ps --all --no-trunc
docker inspect <container>
docker logs --timestamps --tail 300 <container>
docker events --since 30m
Container exits immediately
Inspect the exit record:
docker inspect --format \
'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}} oom={{.State.OOMKilled}}' \
<container>
Common causes include:
- the main process completed successfully;
- an incorrect
ENTRYPOINTorCMD; - a missing file or executable;
- invalid configuration;
- a dependency was unavailable during startup;
- permission failure under the configured user; or
- memory termination.
Run the image with an alternative entrypoint only as a diagnostic step:
docker run --rm --entrypoint sh <image>
Do not permanently replace a failing application with a shell or sleep command.
Exit code 137 or OOMKilled
Exit code 137 can result from a forced kill and is commonly associated with memory pressure, but confirm rather than assume:
docker inspect --format \
'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' \
<container>
docker stats --no-stream
Review container limits, host memory, application heap configuration, concurrency, recent releases, and memory trends. Raising the limit without understanding growth may delay another failure.
Port is already allocated
If Docker cannot publish a port:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Then identify the host process or container using that port. Choose a different host port or stop the conflicting workload after verifying ownership and impact.
Remember that only one listener can normally bind the same host address, protocol, and port combination.
Application is unreachable
Check each boundary:
- Is the container running?
- Is the application listening inside it?
- Is it listening on
0.0.0.0rather than only127.0.0.1inside the container? - Is the container port correct?
- Is the port published on the expected host address?
- Does a host firewall or proxy block the request?
- Does the health endpoint report a dependency failure?
Inspect mappings:
docker port <container>
docker inspect --format '{{json .NetworkSettings.Ports}}' <container>
Container cannot resolve another service
Verify both containers share the intended user-defined network:
docker network inspect <network>
Use the service or container name instead of a transient IP address. Confirm the application is not trying to reach another container through localhost.
If external DNS fails, compare DNS configuration inside the container with host and daemon settings. Avoid hard-coding public resolvers without considering private DNS and organizational policy.
Permission denied on a mounted path
Collect:
docker inspect --format '{{.Config.User}}' <container>
docker inspect --format '{{json .Mounts}}' <container>
Compare the container user and group IDs with the ownership and permissions of the mounted path. On hosts with SELinux or other mandatory access controls, labels and policies may also apply.
Do not make the directory world-writable as a default fix. Align ownership, use an appropriate group, mount a specific writable path, or update the application design.
Image pull or registry authentication fails
Separate these possible causes:
- incorrect registry hostname or repository path;
- missing or expired credentials;
- permission to authenticate but not pull the repository;
- a proxy, firewall, DNS, or TLS trust failure;
- a missing tag or manifest;
- unsupported image architecture; or
- registry rate or policy limits.
Confirm the exact reference and authenticate through the approved mechanism. Do not paste tokens into the command line or shared incident logs.
Build cache appears stale
First determine whether the required source file is present in the build context and not excluded by .dockerignore. Then inspect Dockerfile layer order and build output.
Use a no-cache build as a diagnostic comparison:
docker build --no-cache --progress=plain --tag cloudforge-api:debug .
If it succeeds, identify the incorrectly cached or non-deterministic step. Do not make --no-cache the permanent default without understanding the cause, because it discards useful reproducibility and performance benefits.
no space left on device
Inspect Docker disk usage:
docker system df --verbose
docker image ls
docker container ls --all
docker volume ls
Remove specific unused artifacts only after confirming ownership and recovery requirements:
docker container rm <stopped-container>
docker image rm <unused-image>
docker volume rm <unused-volume>
Broad prune commands can remove caches, stopped containers, images, networks, or volumes needed by other work. Treat them as destructive maintenance, review their scope, and back up persistent data first.
exec format error
This often means the executable or image architecture does not match the runtime, or a script has an invalid shebang or line ending.
Check:
- image OS and architecture;
- host architecture;
- multi-platform manifest entries;
- executable permissions;
- the first line of an entrypoint script; and
- Windows versus Unix line endings.
Build and test on the required platform before release.
Docker daemon is unavailable
Symptoms include “Cannot connect to the Docker daemon” or a named-pipe/socket connection failure.
Check:
docker context ls
docker context show
docker version
Then verify Docker Desktop or the Docker Engine service is running, the active context is correct, and the current user is authorized to access the endpoint. Do not loosen socket permissions globally as a shortcut.
Troubleshooting decision table
| Symptom | First evidence | Likely boundary |
|---|---|---|
| Build cannot find a file | context path and .dockerignore | build context |
| Container exits | state, exit code, logs | command or application startup |
| Restart loop | restart count and timestamped logs | startup or dependency |
| HTTP connection refused | listen address and port mapping | process or networking |
| Service name does not resolve | network membership | Docker DNS or network |
| Data disappears | mount type and target | storage design |
| Pull returns unauthorized | registry identity and repository scope | authentication or authorization |
| Exit 137 | OOM flag and memory metrics | resource pressure |
| Permission denied | runtime user and mount ownership | filesystem or policy |
| Exec format error | image and host architecture | platform compatibility |
Safe cleanup
Start by listing what exists:
docker ps --all
docker image ls
docker volume ls
docker network ls
docker system df
Remove named targets rather than using broad cleanup when possible. Volumes may contain the only copy of application data. Image caches may be shared by active development or CI workflows.
For Compose projects, docker compose down is normally safer than manually deleting generated resources. Review flags before including images or volumes.
Production readiness checklist
Build and image
- The build context is minimal and has a reviewed
.dockerignore. - The Dockerfile uses a trusted, maintained base image.
- Dependencies are installed reproducibly from a lockfile.
- Build tools are excluded from the runtime stage when unnecessary.
- No credentials or private keys exist in image layers.
- The image has been tested, scanned, and identified by digest.
- Supported CPU architectures are explicit and tested.
Runtime security
- The main process runs as a non-root user.
- Privileged mode is not enabled.
- Linux capabilities and mounts are minimized.
- The root filesystem is read-only where practical.
- Writable paths are deliberate and capacity-managed.
- CPU, memory, and process behavior have been measured and constrained.
- Docker daemon access is tightly controlled.
Reliability and operations
- Startup, readiness, liveness, and dependency behavior are understood.
- The application handles termination signals and graceful shutdown.
- Logs are structured, centralized, retained, and free of secrets.
- Metrics and traces expose saturation, errors, latency, and dependencies.
- Persistent data has a tested backup and restore process.
- Restart behavior does not hide a crash loop.
- Deployment and rollback use immutable image digests.
- Runbooks identify owners, alerts, verification, and rollback steps.
Frequently asked questions
What is the difference between an image and a container?
An image is an immutable package used to create containers. A container is a runtime instance with a process, configuration, network attachments, mounts, and a writable layer.
Should I use the latest tag?
Avoid relying on it for controlled releases. Use meaningful immutable release tags and deploy by digest where reproducibility matters.
Does EXPOSE publish a port?
No. It documents the intended container port. Use --publish, a Compose ports entry, or the deployment platform's networking configuration to make a port reachable.
Should every container run one process?
The useful principle is one clear responsibility and one lifecycle-controlling main process. Helper processes may be justified, but independent services are usually easier to scale, secure, observe, and update separately.
Are environment variables safe for secrets?
They can be exposed through process configuration, inspection output, logs, crash reports, or platform interfaces. Prefer the target platform's approved secret mechanism and grant access through workload identity where possible.
Why does my application work locally but fail in Docker?
Common differences include filesystem paths, case sensitivity, CPU architecture, missing runtime dependencies, listen address, environment variables, file permissions, network names, certificates, and assumptions about local tools.
When should I use Docker Compose?
Compose is excellent for defining repeatable multi-container environments and can serve controlled single-host deployments. Use a managed container platform or orchestrator when the workload needs multi-host scheduling, advanced rollout controls, autoscaling, or stronger platform-level availability.
Is a container automatically secure?
No. Security depends on image provenance, patching, secrets, host configuration, daemon access, user identity, capabilities, mounts, networking, resource controls, application security, monitoring, and operational discipline.
Official Docker references
Use the official documentation to confirm current behavior and platform-specific instructions:
- Get started with Docker
- Docker architecture overview
- Dockerfile reference
- Building best practices
- Multi-stage builds
- Docker Compose
- Using Compose in production
- Docker Engine security
- Rootless mode
- Protect Docker daemon access
Final operating principle
Build once, test the actual artifact, promote it by digest, run it with minimum privilege, keep durable state outside the container layer, and diagnose failures from observed state before changing the environment.