Dockerizing a Node.js Web App for Dev and Prod Environments
Dockerizing a Node.js server ensures consistency, scalability, and portability across environments. Learn how to set up separate Dockerfiles for production and development, using Docker Compose for efficient container management and hot-reloading during development.

Dockerizing a Node.js application allows you to encapsulate your server in an isolated, portable environment, ensuring consistency across development, testing, and production stages. In this blog post, we'll learn how to efficiently Dockerize a Node.js Express TypeScript server for both development and production environments.
We’ll set up two Dockerfiles: one optimized for production, and one tailored for development. Additionally, we’ll use Docker Compose to streamline running containers for different environments.
Why Dockerize a Node.js Express TypeScript Server?
Dockerizing a Node.js server offers multiple benefits:
- Consistency: Same environment across development, staging, and production.
- Isolation: Ensures dependencies are properly managed within the container.
- Scalability: Containers can be easily scaled, restarted, or deployed.
- Portability: Docker containers can run anywhere — your machine, cloud platforms, or other environments.
Let’s dive into how to set this up with Node.js, Express, and TypeScript.
Project Structure
Here’s the general project structure we’ll be working with:
.
├── .devcontainer/
├── node_modules/
├── src/
│ ├── configs/
│ ├── cron/
│ ├── domains/
│ ├── libs/
│ ├── middlewares/
│ ├── routes/
│ ├── templates/
│ ├── utils/
│ ├── index.ts
│ └── server.ts
├── .dockerignore
├── .env
├── .gitignore
├── .prettierignore
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.dev
├── nodemon.json
├── package.json
├── README.md
├── tsconfig.json
└── yarn.lock
If you are unaware of this directory structure, you might want to check this article

Step 1: Setting Up the Dockerfiles
We’ll use two Dockerfiles — one for production and another for development.
Dockerfile for Production
The production Dockerfile is optimized to build and run the application efficiently.
# Use an official Node.js runtime as a base image
FROM node:22-alpine AS base
# Set the working directory inside the container
WORKDIR /secure-password
# Copy package.json and yarn.lock into the container
COPY package.json yarn.lock ./
# Install project dependencies
RUN yarn install
# Copy the entire project into the container
COPY . .
# Build the project
RUN yarn build
# Expose the application port
EXPOSE 5656
# Start the application
CMD ["yarn", "start"]
WORKDIR
: Sets the working directory in the container to/secure-password
.COPY package.json yarn.lock ./
: We copypackage.json
andyarn.lock
first to avoid re-running dependency installation on every code change.RUN yarn install
: Installs dependencies.COPY . .
: Copies the rest of the application code.RUN yarn build
: Compiles TypeScript code into JavaScript.CMD ["yarn", "start"]
: Runs the production-ready application.
Dockerfile for Development
The development Dockerfile uses nodemon to restart the server when files change automatically.
# Use an official Node.js runtime as a base image
FROM node:22-alpine AS base
# Set the working directory inside the container
WORKDIR /secure-password
# Copy package.json and yarn.lock into the container
COPY package.json yarn.lock ./
# Install project dependencies
RUN yarn install
# Copy the entire project into the container
COPY . .
# Build the project
RUN yarn build
# Expose the application port
EXPOSE 5656
# Start the application
CMD ["yarn", "dev"]
CMD ["yarn", "dev"]
: In the dev environment, we useyarn dev
, which typically runsnodemon
to automatically restart the server on file changes.
If you are wondering what are these two commands, here are the scripts in the package.json
"scripts": {
"dev": "nodemon src/index.ts",
"build": "tsc && copyfiles -u 1 src/templates/**/* dist",
"start": "node dist/index.js",
"format": "prettier . --write"
},
Step 2: Creating the Docker Compose File
Docker Compose allows us to define and manage multiple environments (development, staging, production) using a single YAML file.
version: "3.8"
services:
prod:
container_name: app_name
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
ports:
- "5656:5656"
volumes:
- .:/app
command: yarn start
dev:
container_name: app_name_dev
build:
context: .
dockerfile: Dockerfile.dev
environment:
NODE_ENV: development
ports:
- "5656:5656"
volumes:
- .:/app
- /app/node_modules
command: yarn run dev
Key Parts of the Docker Compose Setup:
prod
service:- Uses the production Dockerfile (
Dockerfile
). - Runs the application in production mode on port
5656
.
- Uses the production Dockerfile (
staging
service:- This mirrors the production environment but can be used for testing in a staging environment.
- It uses the same
Dockerfile
as production and shares the samecommand: yarn start
.
dev
service:- Uses the development Dockerfile (
Dockerfile.dev
). - Exposes port
5656
and mounts the host directory to the container, allowing for live reload with nodemon. - The
node_modules
directory is excluded from mounting since we want the container to manage its dependencies.
- Uses the development Dockerfile (
Step 3: Adding the .dockerignore
File
Just like .gitignore
, the .dockerignore
file prevents unnecessary files from being copied into the container. This improves build performance and reduces the image size.
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
Dockerfile*
.dockerignore
.git
.env
Step 4: Running the Application
With everything in place, we can now build and run the containers for different environments.
Running in Production Mode
docker-compose up prod
This command will:
- Build the image using the production Dockerfile.
- Start the container and expose it on port
5656
.
Running in Dev Mode
docker-compose up dev
This command will:
- Build the image using the development Dockerfile.
- Run nodemon inside the container for hot-reloading.
- Mount the project directory so that code changes are reflected without rebuilding the image.
Dockerizing a Node.js Express TypeScript application for both development and production environments gives you flexibility and consistency across different stages of your project lifecycle. With the combination of two Dockerfiles (one for dev, one for prod) and a well-structured Docker Compose setup, your application becomes easier to develop, test, and deploy.
Here’s a quick recap:
- Production Dockerfile: Builds the app for optimized production use.
- Development Dockerfile: Supports hot-reloading via nodemon.
- Docker Compose: Simplifies running different containers for development, staging, and production.
This setup is scalable and maintainable, ensuring that your development workflow is smooth and your production environment is efficient. By using Docker effectively, you can focus on writing code, not worrying about inconsistencies between different environments!
That's it for today. See ya 👋