Containers28 min read

Docker for cloud application delivery

Learn Docker images, Dockerfiles, containers, Compose, networking, storage, registries, security, CI/CD, production operations, and troubleshooting.

Production-aware guide

Review scope, permissions, impact, and rollback before applying changes.

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:

  1. The Docker client accepts commands such as docker build and docker run.
  2. The Docker daemon builds images and manages containers, networks, and volumes.
  3. A registry stores and distributes images.
  4. An image is an immutable package assembled from filesystem layers and metadata.
  5. 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:

StateMeaningUseful command
CreatedDefined but not starteddocker start <name>
RunningMain process is activedocker ps
PausedProcesses are suspendeddocker unpause <name>
ExitedMain process endeddocker ps --all
RestartingRestart policy is starting it againdocker logs <name>
DeadDocker could not remove or restart it cleanlyinspect 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

InstructionPurposeProduction note
FROMSelects a base imageuse trusted, maintained images
WORKDIRSets the working directoryprefer it over repeated cd commands
COPYCopies files from build contextuse explicit paths and ownership
RUNExecutes a build stepcombine related package operations
ARGSupplies build-time valuesnot suitable for secrets
ENVSets runtime environment defaultsvalues remain visible in image metadata
EXPOSEDocuments a listening portdoes not publish the port
USERSelects the runtime useruse a non-root identity
HEALTHCHECKDefines container health evaluationkeep checks lightweight and meaningful
ENTRYPOINTDefines the executableuseful for a stable application command
CMDDefines default arguments or commanduse 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 build produces dist;
  • 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:

  1. Check out a specific revision.
  2. Restore only controlled caches.
  3. Run linting, unit tests, and dependency checks.
  4. Build the image once from a reviewed Dockerfile.
  5. Test the built image, including startup and health behavior.
  6. Scan dependencies and operating system packages.
  7. Generate and retain build metadata or a software bill of materials where required.
  8. Sign or attest the artifact if the supply-chain policy requires it.
  9. Push an immutable release tag and record its digest.
  10. Deploy the same digest to later environments.
  11. 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-privileges where supported.
  • Apply memory, CPU, and process limits.
  • Keep the default seccomp profile unless a reviewed requirement says otherwise.
  • Never use --privileged as 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.sock into 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:

  1. Confirm the exact command, host, context, image reference, and timestamp.
  2. Determine whether the failure occurs during build, pull, create, start, health evaluation, or request handling.
  3. Capture container state, exit code, error, OOM status, and restart count.
  4. Read application and daemon logs around the same timestamp.
  5. Inspect mounts, environment, networks, published ports, user, and resource limits.
  6. Reproduce with the smallest safe input.
  7. Change one variable and compare the result.
  8. 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 ENTRYPOINT or CMD;
  • 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:

  1. Is the container running?
  2. Is the application listening inside it?
  3. Is it listening on 0.0.0.0 rather than only 127.0.0.1 inside the container?
  4. Is the container port correct?
  5. Is the port published on the expected host address?
  6. Does a host firewall or proxy block the request?
  7. 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

SymptomFirst evidenceLikely boundary
Build cannot find a filecontext path and .dockerignorebuild context
Container exitsstate, exit code, logscommand or application startup
Restart looprestart count and timestamped logsstartup or dependency
HTTP connection refusedlisten address and port mappingprocess or networking
Service name does not resolvenetwork membershipDocker DNS or network
Data disappearsmount type and targetstorage design
Pull returns unauthorizedregistry identity and repository scopeauthentication or authorization
Exit 137OOM flag and memory metricsresource pressure
Permission deniedruntime user and mount ownershipfilesystem or policy
Exec format errorimage and host architectureplatform 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:

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.