Express Dockerfile for Production (Node.js Alpine, Non-Root)

This Express.js Dockerfile installs production dependencies with npm ci in a separate stage, then runs your server on node:24-alpine as the built-in node user. It listens on port 3000 and contains no development packages. It fits plain JavaScript Express APIs and server-rendered apps that need no build step.

CI status
Build passing
Image size (uncompressed)
174.2 MB
Build time
6 s
Runtime image
node:24-alpine
Port
3000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
.dockerignore
.git
node_modules/
dist/
.env*
npm-debug.log*
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM node:24-alpine AS deps

    A separate stage installs dependencies, so npm's cache and any temporary files stay out of the final image.

  2. COPY package.json package-lock.json ./ RUN npm ci --omit=dev

    Installs exactly the locked versions, skipping devDependencies such as test runners and linters. The layer stays cached until the manifests change.

  3. ENV NODE_ENV=production

    Express and many middleware packages switch to faster, less verbose behavior in production mode, for example by caching view templates.

  4. COPY --from=deps --chown=node:node /app/node_modules ./node_modules

    Copies the production dependencies into the runtime stage, owned by the unprivileged node user.

  5. COPY --chown=node:node . .

    Adds your application code. .dockerignore keeps your local node_modules and environment files from overwriting the clean install.

  6. USER node EXPOSE 3000 CMD ["node", "server.js"]

    Runs as the non-root user that ships with the official Node image, and starts node directly so stop signals reach your server.

Build and run

terminal
docker build -t express-app .
docker run --rm -p 3000:3000 express-app

Build from the project root and open http://localhost:3000. Pass configuration such as database URLs with -e flags or an env file instead of baking it into the image.

Other Express Dockerfile variants

This variant suits plain JavaScript. Express apps written in TypeScript need a build stage that compiles to dist first, similar to the NestJS and Hono files on this site. Verified Express variants appear in the table below.

VariantRuntime imageImage sizeCI
Node Alpine (production deps only)node:24-alpine174.2 MBPassing

Troubleshooting

Error: Cannot find module 'express'
Express is listed under devDependencies, so --omit=dev skipped it. Move every package your server imports at runtime into dependencies.
npm ci can only install packages when your package.json and package-lock.json are in sync
The lockfile is outdated. Run npm install locally, commit the updated package-lock.json, and rebuild the image.
The container ignores docker stop and is killed after a delay
Handle SIGTERM in server.js by closing the HTTP server, then exiting. Starting node directly, as this file does, ensures the signal arrives.

FAQ

Why use npm ci instead of npm install?
npm ci installs exactly what package-lock.json specifies and fails if the lockfile is out of date. That makes builds reproducible, which npm install doesn't guarantee.
Do I need a process manager like PM2?
Usually not. Docker or your orchestrator restarts crashed containers. To use several CPU cores, run more containers or use Node's cluster module.
Is Alpine safe for Express?
Yes for most apps. If a native dependency fails on Alpine's musl libc, switch the base image to node:24-slim, which uses glibc.