✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.2 Container Practices

A focused guide to Container Practices, connecting core concepts with practical Docker and container operations.

Container practices cover the runtime-level decisions made about how a container is actually configured and operated once started, distinct from image practices which concern how the underlying image is built, focused on treating containers as disposable, immutable units that can be replaced freely rather than as long-lived, individually maintained systems.

Treating containers as disposable, not as pets

A container should be replaceable at any moment without ceremony, the underlying assumption that makes scaling, rolling deployment, and automatic recovery from failure possible at all; this requires designing the application itself, and the surrounding configuration, around the expectation that any given container instance may be destroyed and recreated without warning:

docker rm -f my-api
docker run -d --name my-api my-api:1.4.2

An application that stores meaningful, irreplaceable state only inside its own container, rather than in a volume or external service, violates this assumption directly, and most operational pain around containerized applications traces back to some violation of this core, disposability principle somewhere in the system.

Never modifying a running container in place

Once a container is running, it should not be modified directly through docker exec to install something, change a configuration file, or patch a bug; any genuine change should be made to the image or its configuration and rolled out as a new container instance, since a manually patched, running container creates undocumented, untracked drift that the next deployment or replacement will silently discard:

docker exec my-api apt-get install -y curl
RUN apt-get update && apt-get install -y curl

The first approach creates a temporary fix that exists only in this one specific, running container and disappears the moment it is replaced; the second makes the change a permanent, version-controlled part of the image itself, which is the only approach that survives the container's inevitable, eventual replacement.

Explicit, deliberate resource limits

Every container running in any shared environment, which is to say nearly any production environment, should have explicit memory and CPU limits configured rather than left unbounded, protecting every other workload sharing the same host from being starved by any single misbehaving or unexpectedly resource-hungry container:

docker run -d --memory=512m --cpus=1 my-api

Sizing these limits based on actual, observed usage under realistic load, reviewed and adjusted periodically as the application's actual resource needs evolve, rather than set once and never revisited.

Restart policies matched to the container's actual role

A long-running service container generally warrants an automatic restart policy, while a one-off, task-oriented container generally should not be automatically restarted at all, since a failed one-off task restarting indefinitely produces a confusing, repeating failure rather than a useful recovery:

docker run -d --restart=unless-stopped my-api
docker run --rm my-api npm run migrate

Matching the restart policy to the container's actual intended role, rather than applying the same default to every container regardless of purpose, avoids both an unrecovered, persistently down service and an endlessly retrying, never-succeeding one-off task.

Idempotent startup behavior

A container's startup sequence should be safe to run repeatedly, including after an unclean shutdown or a forced restart mid-operation, since a container can be killed and restarted at any point in its lifecycle, and startup logic that assumes a clean, orderly prior shutdown will eventually encounter a situation where that assumption does not hold:

async function runMigrationsIdempotently() {
  await db.query('CREATE TABLE IF NOT EXISTS migrations (...)');
  // check which migrations have already run before applying new ones
}

Designing startup logic, particularly anything involving state initialization or migration, to be safely re-runnable rather than assuming it only ever executes exactly once in a clean, predictable sequence, avoids a class of bug that only manifests after an unexpected restart.

Explicit configuration over implicit defaults

Configuration values that affect a container's behavior should be supplied explicitly, through environment variables or mounted configuration files, rather than relying on whatever implicit default the application or base image happens to apply, since an explicit value is visible, auditable, and intentional in a way an implicit default is not:

docker run -e LOG_LEVEL=info -e NODE_ENV=production my-api

This also makes a container's actual, effective configuration directly inspectable through docker inspect, rather than requiring knowledge of the application's internal default behavior to understand what it is actually doing.

Meaningful health checks tied to real readiness

A container's health check should reflect genuine readiness to serve its actual function, not merely confirm the process is technically running, since the entire value of health checking depends on it actually detecting the failures that matter:

HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:3000/healthz || exit 1

Graceful shutdown handling as a default expectation

Every long-running container should handle SIGTERM correctly, finishing in-flight work within a reasonable timeout rather than relying on the eventual forced SIGKILL, since termination is a routine, frequent event in a containerized environment, not an exceptional one.

process.on('SIGTERM', () => server.close(() => process.exit(0)));

Logging to stdout and stderr, never to an internal file

Application output should go to stdout and stderr, letting Docker's own logging mechanism capture and route it, rather than to an internal log file that disappears along with the container and provides no path to centralized aggregation.

console.log(JSON.stringify({ level: 'info', message: 'Server started' }));

Common mistakes

  • Storing meaningful, irreplaceable application state only within a container's own writable layer, violating the core assumption that any container can be replaced freely at any time.
  • Patching a running container directly with docker exec rather than making the change part of the image and redeploying a new container instance.
  • Leaving containers without explicit resource limits, allowing any single one to consume more shared host capacity than intended.
  • Writing startup logic that assumes a clean, orderly prior shutdown, breaking when the container is instead restarted unexpectedly mid-operation.
  • Relying on implicit application or base image defaults for configuration that should instead be supplied explicitly and visibly.

Container practices treat each running container as a disposable, replaceable instance rather than an individually maintained system, which shapes nearly every other specific decision, resource limits, restart policy, idempotent startup, explicit configuration, graceful shutdown, around the expectation that the container in front of you right now may not be the one running an hour from now, and that this is by design, not a problem to work around.

Content in this section