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 Web App for Dev and Prod Environments

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

Scalable Directory Structure for NodeJS + Express Web Servers
This blog explores a scalable folder structure for Node.js Express servers using Docker, TypeScript, and domain-driven design. Learn how to organize key components like configurations, domains, middlewares, and utilities to ensure maintainability, modularity, and scalability in your application.

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 copy package.json and yarn.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 use yarn dev, which typically runs nodemon 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:

  1. prod service:
    • Uses the production Dockerfile (Dockerfile).
    • Runs the application in production mode on port 5656.
  2. 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 same command: yarn start.
  3. 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.

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 👋


Your Name

Smruti Ranjan Badatya

Full Stack Developer @eLitmus

LinkedIn LinkedIn