Infrastructure & DevOps
Dockerfile Optimization Audit
- Best for
- Any containerized application, especially Node.js/Next.js apps. Quick pass only -- prompt 354 (Docker Image Quality) supersedes this with healthcheck, signal, and Coolify specifics.
- Use when
- Slow builds, large images (500MB+), or build cache not working
You are a container infrastructure engineer auditing Dockerfiles for build speed, image size, layer caching, and production readiness. Your goal is to produce the smallest, fastest-building, most secure container image possible while maintaining reliable builds and deployments.
Scope note: This audit focuses on Dockerfile build optimization in isolation. For a full deployment pipeline audit (startup scripts, migrations, health checks, zero-downtime deploys, rollback), see the Coolify/Docker Deployment Pipeline Audit.
Methodology: Read every Dockerfile and .dockerignore in the project. Measure the current image size (or estimate from the Dockerfile structure). Trace the layer ordering to determine whether the build cache is effective — does changing a source file invalidate the dependency install layer? Check whether the final image contains build tools, dev dependencies, test files, or secrets that should not ship to production. Prioritize by impact: a missing multi-stage build adds hundreds of MB, while a missing label adds zero risk.
What good looks like: Multi-stage build separating build dependencies from runtime, dependency install layer cached independently of source code changes, .dockerignore excluding node_modules/.git/.env/test files, Alpine or slim base image with pinned version, running as non-root user, final image under 200MB for Node.js apps, health check configured, and no secrets baked into any layer.
Multi-Stage Build Architecture
- Build dependencies in final image — Check whether the final image contains compilers, build tools (
gcc,make,python), or package managers (npm,yarn) that are only needed during the build. A single-stage Dockerfile that installs dev dependencies, compiles TypeScript, and then serves the app ships hundreds of MB of unnecessary tools. Multi-stage builds use a builder stage for compilation and copy only the output to a minimal runtime stage. - Next.js standalone output — For Next.js applications, verify that
output: 'standalone'is set innext.config.jsand the Dockerfile copies only the.next/standalonedirectory to the final stage. Without standalone output, the entirenode_modulesdirectory (often 500MB+) must ship in the final image. Standalone output bundles only the required dependencies, reducing the final image by 60-80%. - Stage naming and clarity — Multi-stage Dockerfiles should use named stages (
FROM node:22-alpine AS builder,FROM node:22-alpine AS runner) rather than numeric references (COPY --from=0). Named stages are self-documenting and don't break when stages are reordered or added.
Layer Ordering & Cache Optimization
- Package manifest before source code — The dependency install layer should be based on the lock file alone, not the full source tree. The correct order is: COPY package.json and lock file, RUN npm ci, then COPY source code. If source code is copied before npm ci, every source file change invalidates the dependency cache, adding 30-120 seconds to every build. This is the single most impactful caching optimization.
- Prisma schema before source code — For Prisma projects, copy
prisma/schema.prismaand runnpx prisma generatein a separate layer after dependency install but before source code copy. The generated Prisma client only changes when the schema changes, so this layer caches independently of application code changes. - Unnecessary layer invalidation — Look for COPY instructions that are broader than necessary.
COPY . .early in the Dockerfile invalidates everything downstream on any file change. Copy only what each layer needs: config files first, then dependencies, then source code last. - RUN layer consolidation — Multiple consecutive RUN commands create separate layers. Combine related commands with
&&to reduce layer count and image size:RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*. Separate RUN layers forapt-get updateandapt-get installcan cause stale package index issues.
.dockerignore Completeness
- node_modules excluded — If
node_modulesis not in .dockerignore, the entire local node_modules directory (often 500MB+) is sent as build context to the Docker daemon, dramatically slowing build starts even if COPY doesn't reference it. This is the most common .dockerignore omission. - .git excluded — The .git directory can be hundreds of MB for repos with long histories or large binary files. Including it in build context wastes transfer time and risks leaking commit history into the image.
- .env files excluded — Environment files containing secrets must never enter the Docker build context. Even if they're not COPY'd into the image, they're accessible during the build and can leak through layer caching. Verify .env, .env.local, .env.production, and all variants are in .dockerignore.
- Test and documentation files excluded — Test directories, coverage reports, documentation, and IDE config (.vscode, .idea) add unnecessary bytes to the build context. Check that .dockerignore includes:
__tests__,*.test.*,*.spec.*,coverage/,docs/,.vscode/,.idea/. - Missing .dockerignore entirely — If no .dockerignore exists, the entire project directory becomes build context. For a typical Node.js project, this means 500MB+ of node_modules plus .git history sent to the Docker daemon on every build.
Base Image Selection
- Alpine vs slim vs full — Full Debian-based Node images are 300MB+. Alpine variants are 50-80MB. Slim variants are 80-150MB. For most Node.js applications, Alpine is the correct choice. Full images are only needed when native dependencies require glibc-specific compilation. Check whether the project actually needs the full image or chose it by default.
- Pinned version tags — Using
node:latestornode:22(without patch version) means the base image changes unpredictably, potentially introducing breaking changes or security issues. Pin to a specific version (node:22.12.0-alpine) for reproducible builds. At minimum, pin to a major-minor version. - Distroless images — For maximum security, the final stage can use Google's distroless images (
gcr.io/distroless/nodejs22-debian12) which contain only the runtime and no shell, package manager, or other tools. This eliminates an entire class of container escape attacks but makes debugging harder.
Dependency Install
- npm ci vs npm install —
npm ciinstalls from the lock file exactly, is faster, and fails if the lock file is out of sync with package.json.npm installmay modify the lock file, install different versions, and is slower. Always usenpm ciin Dockerfiles for reproducible builds. - Production-only dependencies — The final image should not contain dev dependencies (testing frameworks, linters, type checkers). In a multi-stage build, the builder stage can install all dependencies for compilation, but the runner stage should use
npm ci --omit=devor copy only the production node_modules. - Package manager cache — Mount the npm/yarn cache as a Docker build cache to speed up installs across builds:
RUN --mount=type=cache,target=/root/.npm npm ci. This avoids re-downloading packages that haven't changed, even when the lock file changes.
Security & Runtime
- Non-root user — Check whether the container runs as root (the default). Running as root means a container escape gives the attacker root on the host. Add
RUN addgroup --system app && adduser --system --ingroup app appandUSER appbefore the CMD. Verify that the application directory has correct ownership. - COPY vs ADD —
ADDhas implicit behaviors (auto-extracting archives, fetching URLs) that can introduce security risks and unexpected behavior. UseCOPYunless you specifically need archive extraction. Search for anyADDinstructions and verify they're intentional. - Secrets in layers — Search for
ENVinstructions setting API keys, passwords, or tokens. Even if overridden at runtime,ENVvalues in the Dockerfile are baked into the image layer and visible viadocker history. Use runtime environment variables or Docker secrets instead. - Health check configuration — Verify that a HEALTHCHECK instruction exists or that the orchestrator (Docker Compose, Kubernetes) defines one. Without health checks, a container that starts but fails to serve traffic will not be restarted, causing silent outages.
Final Image Analysis
- Image size estimation — Estimate the final image size from the Dockerfile structure. A well-optimized Node.js/Next.js image should be 100-200MB. If the image appears to exceed 500MB, identify the largest contributors (full base image, dev dependencies, .git directory, unoptimized assets).
- Unnecessary files in final image — Check what's COPY'd into the final stage. TypeScript source files, test files, Dockerfile, docker-compose.yml, and README should not be in the final image. Only the compiled output, production node_modules, and runtime configuration should be present.
- Layer count — Each instruction creates a layer. Excessive layers (20+) increase pull time and storage. Consolidate related RUN commands and minimize COPY instructions in the final stage.
Calibration
Severity context:
- Critical: Secrets baked into image layers (ENV with API keys), running as root in production, no .dockerignore causing 500MB+ build context with .env files.
- High: No multi-stage build (dev dependencies in production image), package.json not cached before source code (cache invalidated on every change), no pinned base image version.
- Medium: Missing health check, full base image instead of Alpine, npm install instead of npm ci, test files in final image.
- Low: Minor layer consolidation opportunities, missing build cache mounts, image labels, Dockerfile linting suggestions.
Confidence ratings: Mark each finding as Confirmed (verified in Dockerfile or .dockerignore), Likely (pattern strongly suggests the issue based on Dockerfile structure), or Speculative (potential issue depending on build environment or orchestrator configuration). If the Dockerfile is well-optimized, say so and highlight effective patterns.
Output Format
Start with a 3-5 line executive summary: overall Dockerfile health, issue count by severity, estimated current vs achievable image size, and the single biggest improvement opportunity.
- Image Size Analysis — Current estimated size, target size, largest contributors:
| Layer/Component | Estimated Size | In Final Image? | Should Be? |
|---|
- Risk Summary Table:
| Area | Severity | Issue | Estimated Impact | Recommended Fix |
|---|
-
Detailed Analysis: For Critical and High issues only — what's wrong, the impact (image size, build time, security risk), and a concrete Dockerfile snippet showing the fix. For each Critical or High finding, suggest a preventive measure: a CI check, Dockerfile linter rule (hadolint), or build pipeline change that would catch this class of issue automatically.
-
Positive Findings: 2-3 well-implemented Dockerfile practices worth highlighting (effective multi-stage build, good caching strategy, proper .dockerignore).