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
# 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"].git
node_modules/
dist/
.env*
npm-debug.log*
Dockerfile
.dockerignoreBefore you build
- Commit package-lock.json so npm ci installs exact versions.
- Use server.js as the entry point, or change the CMD line to your file.
- Make the app listen on process.env.PORT, falling back to 3000.
How this Dockerfile works
FROM node:24-alpine AS depsA separate stage installs dependencies, so npm's cache and any temporary files stay out of the final image.
COPY package.json package-lock.json ./ RUN npm ci --omit=devInstalls exactly the locked versions, skipping devDependencies such as test runners and linters. The layer stays cached until the manifests change.
ENV NODE_ENV=productionExpress and many middleware packages switch to faster, less verbose behavior in production mode, for example by caching view templates.
COPY --from=deps --chown=node:node /app/node_modules ./node_modulesCopies the production dependencies into the runtime stage, owned by the unprivileged node user.
COPY --chown=node:node . .Adds your application code. .dockerignore keeps your local node_modules and environment files from overwriting the clean install.
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
docker build -t express-app .
docker run --rm -p 3000:3000 express-appBuild 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Node Alpine (production deps only) | node:24-alpine | 174.2 MB | Passing |
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.