AI writes CRUD apps in seconds. It does not design container layers, shrink images, or debug broken builds in CI.
Here are 6 Docker patterns senior JavaScript developers use daily, and mid-level developers usually ignore.
1. Layer Caching With package.json First Cuts Rebuild Time by 70%+
If you copy your entire project before running npm ci, every code change invalidates dependency layers.
Before
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Every change forces a full reinstall.
After
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
Dependencies install only when package.json changes. On a 300 dependency project this drops rebuild time from 90 seconds to under 20 seconds. In CI it compounds even more with caching.
2. Multi-Stage Builds Shrink Next.js Images From 800MB to ~150MB
Build tools do not belong in production images.
Before
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node_modules/.bin/next", "start"]
You ship TypeScript compiler, dev dependencies, and build artifacts.
After
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
CMD ["node_modules/.bin/next", "start"]
The final image contains only production dependencies and compiled output. Smaller image means faster deploys and less attack surface.
This pattern compounds with the standalone output mode covered in the Next.js production scaling guide when you are optimizing startup time and bundle size together.
3. Non-Root Containers Close a Real Security Hole
By default, Node runs as root inside a container.
Before
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --only=production
CMD ["node", "server.js"]
If someone exploits your app, they are root inside the container.
After
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -g 1001 -S nodejs
RUN adduser -S appuser -u 1001
RUN chown -R appuser:nodejs /app
USER appuser
CMD ["node", "server.js"]
Now your Node process runs as a non-root user. If compromised, the blast radius is dramatically smaller. This is a baseline production practice, not an advanced one.
4. Docker Compose Service Names Replace localhost
Most broken Docker setups fail because developers use localhost inside containers.
Before
import { Pool } from 'pg'
const pool = new Pool({
host: 'localhost',
port: 5432,
})
Inside Docker, localhost means the container itself.
After
# docker-compose.yml
services:
api:
build: .
environment:
DATABASE_URL: postgresql://user:password@db:5432/myapp
depends_on:
- db
db:
image: postgres:16-alpine
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
db resolves to the database container via Docker's internal network. This eliminates the entire class of “works locally but not in Docker” connection bugs.
5. Run Tests Inside the Same Image You Deploy
If your tests run on the host and production runs in a container, you are not testing the same environment.
Before
npm test
Runs on macOS or Windows with system-specific behavior.
After
# docker-compose.yml
services:
test:
build:
context: .
target: builder
command: npm test
environment:
NODE_ENV: test
DATABASE_URL: postgresql://user:password@db:5432/test_db
depends_on:
- db
docker compose run --rm test
Now your tests execute inside the same Linux image that production uses. This catches native module issues, missing environment variables, and OS-level differences before they ship.
6. GitHub Actions Layer Caching Cuts CI From 4 Minutes to Under 1
Most teams build Docker images in CI incorrectly and rebuild every layer every time.
Before
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
No caching. Every build starts from zero.
After
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:${{ github.sha }},myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Unchanged layers are restored from GitHub cache. On a typical Next.js project, CI build time drops from 4 minutes to 45 seconds. Multiply that by every PR and every deploy.
What This Actually Signals in Interviews
Anyone can paste a Dockerfile from an AI tool.
Very few developers can explain:
- Why
package.jsonmust be copied before source. - Why multi-stage builds reduce attack surface.
- Why running as root is dangerous.
- Why
localhostbreaks inside containers. - Why tests must run in the deployment image.
- Why CI layer caching matters.
Those are system ownership signals. They separate someone who writes features from someone who ships systems.
If you are mid-level, take your current project and implement these six patterns this week. You do not need Kubernetes. You need repeatable builds, small images, and predictable deployments.
That is what production-ready JavaScript actually looks like in 2026.
Top comments (0)