DEV Community

JSGuruJobs
JSGuruJobs

Posted on

6 Docker Patterns That Turn a Mid-Level JavaScript Dev Into a Production-Ready Engineer

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
import { Pool } from 'pg'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
docker compose run --rm test
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.json must be copied before source.
  • Why multi-stage builds reduce attack surface.
  • Why running as root is dangerous.
  • Why localhost breaks 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)