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
| Concept | What it is |
|---|---|
| Image | Immutable template (layers, files, deps) |
| Container | Isolated process that runs an image |
| Volume | Persistence or shared data when needed |
| Config | Per-environment values (URLs, limits, flags) |
| Secret | Credential 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
| Probe | Question | Typical mistake |
|---|---|---|
| Liveness | Is the process alive? | Restart loop because of an external failure |
| Readiness | Can 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?
| Situation | Reasonable direction |
|---|---|
| Small app / small team | Compose or managed PaaS |
| Several services with mature ops | Platform based on networks, deploys, and cost |
| Kubernetes | Only 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
.envinside 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
- Authentication
- Observability
- SQL, query builders, and ORMs
- How to test an API
- Containers and Twelve-Factor (this note)