A container image packages a Node.js service with the runtime, dependency install, and start command that Docker will run later. For an HTTP app, a successful image build is not enough; the container should start from that image and answer the same smoke-test route the service exposes.
The Dockerfile pattern assumes the project already has package.json, package-lock.json, and a script named start. It fits a service that can run from its source tree with npm start, uses the Node.js 24 LTS Debian slim image, installs production dependencies with npm ci --omit=dev, and switches to the built-in node user before starting the process.
Keep .env files, local node_modules directories, logs, and version-control data out of the build context. Port publishing stays outside the image, so the container can listen on port 3000 while Docker publishes host port 8080 for the smoke test.
Related: How to create a Dockerfile
Related: How to build a Docker image
Related: How to run a Docker container
Tool: Dockerfile Generator
$ cd /srv/orders-api
$ npm pkg get scripts.start "node server.js"
If no start command appears, add a real start script before building the image.
# syntax=docker/dockerfile:1 FROM node:24-bookworm-slim WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm ci --omit=dev COPY --chown=node:node . . USER node EXPOSE 3000 CMD ["npm", "start"]
npm ci --omit=dev installs from package-lock.json and leaves development dependencies out of the runtime image.
node_modules npm-debug.log .env .git Dockerfile .dockerignore coverage *.log
Do not bake secrets into the image. Supply environment-specific values at container run time or through the deployment platform.
$ docker build --tag orders-api:dockerized . [+] Building 12.9s (14/14) FINISHED ##### snipped ##### => [4/5] RUN npm ci --omit=dev => => # up to date, audited 1 package in 504ms => => # found 0 vulnerabilities ##### snipped ##### => naming to docker.io/library/orders-api:dockerized done
$ docker image ls orders-api REPOSITORY TAG SIZE orders-api dockerized 349MB
$ docker run --detach --name orders-api --publish 8080:3000 orders-api:dockerized 0e4fc2aeeb57
--publish 8080:3000 maps host port 8080 to port 3000 inside the container. Use a different host port when 8080 is already in use.
$ docker ps --filter name=orders-api NAMES IMAGE STATUS PORTS orders-api orders-api:dockerized Up 24 seconds 0.0.0.0:8080->3000/tcp, [::]:8080->3000/tcp
$ curl -sS http://127.0.0.1:8080/health
{"status":"ok","service":"orders-api"}
Replace /health with the route that confirms the app is ready to receive traffic.
$ docker rm --force orders-api orders-api