DevelopmentMarch 10, 2026

Docker for Node.js in Development and Production

Our main stack is Node.js/JavaScript and, like many teams, we've moved our development and production environments to Docker containers. However, when we started learning Docker, we found that most articles focused on either development or production, and there was nothing about how to organize your Docker setup to be flexible for both cases.

In this article we'll walk you through different use cases and Dockerfile examples for Node.js, explain the reasoning behind each decision, and help you picture what your workflow with Docker should look like. We'll start with a simple example and then move on to more complex scenarios and solutions to keep your development experience consistent, whether you use Docker or not.

Heads up: this guide is long and aimed at readers with different levels of Docker experience; at times the instructions may seem obvious, but we'll try to add relevant points alongside them so you get a complete picture of the final setup.

Prerequisites

Cases covered

  • Basic Dockerfile and docker-compose for Node.js
  • Nodemon in development, Node in production
  • Keeping devDependencies out of the production Docker image
  • Using a multi-stage build for images that need node-gyp

Add a .dockerignore file

Before setting up the Dockerfile, add a .dockerignore file to your app's folder. The .dockerignore file excludes the files listed in it from the COPY/ADD commands. More info here


node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode

Basic Dockerfile for Node.js

To keep things clear, we'll start with a basic Dockerfile you can use in simple Node.js projects. By simple we mean your code has no additional native dependencies or build logic.


FROM node:10-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "npm", "start" ]

You'll find something similar in pretty much any article about Docker and Node.js. Let's go over it briefly.


WORKDIR / usr / src / app;

The workdir is a sort of default directory used by the RUN, CMD, ENTRYPOINT, COPY, and ADD instructions. In some articles you'll see people run mkdir /app and then set that as the workdir, but that's not a good practice. Use a folder that already exists, /usr/src/app, which is better suited for this.


COPY package*.json ./
RUN npm install

Another good practice: copy package.json and package-lock.json before copying the app's code into the container. Docker will cache the installed node_modules as a separate layer; that way, if you change the app's code and run the build, node_modules won't be reinstalled unless you've touched package.json. In general, even if you forget to add those lines, you won't run into many issues. Usually you only need to run docker build when package.json changes, which means installing from scratch anyway. In other cases, during development you don't tend to run docker build very often after the first build.

This is where docker-compose comes in

Before running the app in production, you need to develop it. The best way to orchestrate and run your Docker environment is with docker-compose. You define, in a YAML file with simple syntax, the list of containers/services you want to run and their instructions.


version: '3'
services:
example-service:
- build: .
- volumes:
- - .:/usr/src/app
- - /usr/src/app/node_modules
- ports:
- - 3000:3000
- - 9229:9229
- command: npm start

In this basic docker-compose.yaml example, the build uses the Dockerfile in the app's folder; then that folder is mounted into the container, and the node_modules installed inside the container during the build aren't overwritten by the ones in your local folder. Port 3000 is exposed to your localhost, assuming you have a web server running. Port 9229 is used to expose the debug port. More info here.

Now run your app with:


docker-compose up

Or use the VS Code extension to do the same thing.

With this command we expose ports 3000 and 9229 of the dockerized app to localhost, mount the current folder with the app at /usr/src/app, and use a small trick so Docker doesn't overwrite the node modules with the ones from the local machine.

So, can you use that Dockerfile in both development and production?

Yes and no.

Differences in CMD

To start, in development you'll normally want the app to reload when a file changes. You can use nodemon for that. But in production you want to run it without that. That means the CMD (command) for development and for production need to be different.

There are a few options to achieve this:

  1. Replace the CMD with the command that runs the app without nodemon, which can be a separate command defined in your package.json, for example:


    "scripts": {
    "start": "nodemon --inspect=0.0.0.0 src/index.js",
    "start:prod": "node src/index.js"
    }

    In that case, your Dockerfile could look like this:


    FROM node:10-alpine
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    CMD [ "npm", “run”, "start:prod" ]

    However, since in development you use the docker-compose file, you can define a different command inside it, just like in the previous example:


    version: '3'
    services:
    ### ... previous instructions
    - command: npm start

  2. If the differences are bigger, or you use docker-compose in both development and production, you can create several docker-compose or Dockerfile files depending on what changes. For example, docker-compose.dev.yml or Dockerfile.dev.

Managing package installation

In general, it's best for the production image to be as lightweight as possible and to avoid installing node modules dependencies that aren't needed in production. This can be solved while keeping a single Dockerfile.

Check your package.json and separate the devDependencies from the dependencies. More info here. In short, if you run npm install with the --production flag or set NODE_ENV to production, no devDependency will be installed. We'll add a few lines to the Dockerfile to handle this:


FROM node:10-alpine
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "npm", “run”, "start:prod" ]

To customize the behavior we use


ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}

Docker lets you pass build arguments from the docker command or from docker-compose. NODE_ENV=development will be used by default as long as we don't override it with another value. You'll find a good explanation here.

Now, when you build the containers with a docker-compose file, all dependencies will be installed, and when you build for production you can pass production as a build argument and devDependencies will be skipped. If you use CI services to build the containers, you just need to add that option to their configuration. More info here

Multi-stage build for images that need node-gyp

Not every app you run in Docker will use only JS dependencies; some need node-gyp and additional native libraries installed on the operating system.

To solve this we can use multi-stage builds, which let you install and compile all dependencies in a separate container and pass only the result of the installation to the final container, with no leftovers. The Dockerfile could look like this:


// The instructions for the first stage
FROM node:10-alpine as builder
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
RUN apk --no-cache add python make g++
COPY package*.json ./
RUN npm install
// The instructions for second stage
FROM node:10-alpine
WORKDIR /usr/src/app
COPY --from=builder node_modules node_modules
COPY . .
CMD [ "npm", “run”, "start:prod" ]

In this example, in the first stage we install and compile all dependencies according to the environment, and in the second, we copy the node_modules we'll use in both development and production.

The line RUN apk --no-cache add python make g++ may vary from project to project, since you'll likely need other dependencies.


COPY --from=builder node_modules node_modules

In that line we copy the node_modules folder from the first stage into the node_modules folder of the second. Since in the second stage we set WORKDIR to /usr/src/app, node_modules will be copied into that folder. Summary

We hope this guide helped you understand how to organize your Dockerfile so it works for both development and production. We can sum up our tips like this:

  • Try to unify the development and production Dockerfile; if that doesn't work, split them.
  • Don't install development node_modules in production builds.
  • Don't leave the native extension dependencies needed for node-gyp and node modules installation in the final image.
  • Use docker-compose to orchestrate your development environment.
  • To orchestrate your Docker containers in production, it's up to you: docker-compose, Docker Swarm, or Kubernetes.

Tell us about your case

Get in touch