Daniel Murillo
Index

Architecture · October 28, 2026 · Daniel Murillo

Containers and Twelve-Factor: from your machine to production

A container makes the environment more reproducible; it does not by itself turn the app into a secure, scalable, easy-to-operate service.

A container makes the environment more reproducible; it does not by itself turn an application into a secure, scalable, or easy-to-operate service.

Introduction

“It works on my machine” usually points to environment differences: dependencies, configuration, permissions, secrets, or external services.

Docker packages the app and system dependencies. Twelve-Factor provides principles for configuring and operating services. Neither replaces design, security, or operations.

1. Image, container, and configuration

ConceptWhat it is
ImageImmutable template (layers, files, deps)
ContainerIsolated process that runs an image
VolumePersistence or shared data when needed
ConfigPer-environment values (URLs, limits, flags)
SecretCredential with controlled access and rotation

Do not put secrets in the image or in build layers. Environment variables inject config, but they are not a vault: control who can read them on the platform.

2. Multi-stage Dockerfile for Node.js

Use a deliberate LTS line. As of late October 2026, Node.js 26 enters Active LTS; 24 is in maintenance. The example pins node:24-bookworm-slim as a conservative base; update when your dependency matrix allows.

FROM node:24-bookworm-slim AS build
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

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 /app/package-lock.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"]

Good practices: pin the version (or digest), .dockerignore, scan for vulnerabilities, small runtime surface, do not copy .env.

node_modules
dist
.git
.env
.env.*
coverage

If you use Bun in development, production runtime can still be Node or Bun depending on what you have validated; do not mix lockfiles and commands unintentionally.

3. Twelve-Factor (what matters most)

See the full list. In practice, these factors avoid pain:

  • Config outside the code
  • Dependencies explicit
  • Build / release / run separated
  • Processes without persistent local state
  • Logs as a stream (stdout/stderr)
  • Disposability: fast start and orderly shutdown
  • Reasonable dev/prod parity

It is not religious dogma: it is an operational design checklist.

4. Configuration that fails fast

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required environment variable: ${name}`);
  return value;
}

export const config = {
  port: Number(process.env.PORT ?? 3000),
  databaseUrl: requiredEnv("DATABASE_URL"),
};

Validate at startup (types, URLs, ranges). Do not print connection strings with credentials.

5. Liveness vs readiness

ProbeQuestionTypical mistake
LivenessIs the process alive?Restart loop because of an external failure
ReadinessCan it receive traffic?Keep sending requests to a broken instance
app.get("/health/live", (_req, res) => {
  res.status(200).json({ status: "alive" });
});

app.get("/health/ready", async (_req, res) => {
  try {
    await database.query("SELECT 1");
    res.status(200).json({ status: "ready" });
  } catch {
    res.status(503).json({ status: "not_ready" });
  }
});

Do not make liveness depend on every dependency: sometimes you want to mark not ready without killing the process.

6. Graceful shutdown

const server = app.listen(config.port);

process.on("SIGTERM", () => {
  server.close(async () => {
    await database.end();
    process.exit(0);
  });
});

In production: drain timeouts, keep-alive, queues, and in-flight work. Neither blind immediate shutdown nor infinite wait.

7. Basic security

  • Non-root user (USER node)
  • Maintained and updated base image
  • Minimal runtime surface
  • Image and dependency scanning
  • CPU/memory limits
  • No secrets in layers, logs, or the repo
  • Do not mount the Docker socket into apps without extreme need

A container is not an absolute sandbox: the host, capabilities, and policies matter.

8. Compose, PaaS, or Kubernetes?

SituationReasonable direction
Small app / small teamCompose or managed PaaS
Several services with mature opsPlatform based on networks, deploys, and cost
KubernetesOnly if the operational complexity is justified

Choose what the team can operate, not just what looks good in a diagram.

9. Common mistakes

  • Persistence only on the container filesystem
  • A different image per environment only because of config
  • Unnecessary root
  • .env inside the image
  • Ignoring SIGTERM
  • A single health check for everything
  • No resource limits
  • Kubernetes “just in case”

10. Production checklist

  • Reproducible image
  • Secrets outside the image
  • Config validated at startup
  • Least privilege
  • Liveness ≠ readiness
  • Graceful shutdown
  • Resource limits
  • Observable logs
  • Replaceable container without losing critical data
  • Deploy + rollback rehearsed

Conclusion

Containers standardize build and execution. Twelve-Factor organizes config, processes, and logs. Reliability appears when you combine them with security, observability, tests, and a platform sized to the problem.

Do not pick the most complex infrastructure in advance. Pick what you can operate well today.

In this series

  1. Authentication
  2. Observability
  3. SQL, query builders, and ORMs
  4. How to test an API
  5. Containers and Twelve-Factor (this note)

Related reading

References