Background

If you’re building apps with NestJS and looking to save time on manual deployments, setting up a CI/CD pipeline is a total game-changer. With the right tools like Docker and GitHub Actions, you can automate your entire build and deployment process, so your app is always production-ready with every push. In this guide, I’ll walk you through the exact steps to create a CI/CD pipeline for your NestJS app using Docker, making your workflow smoother, faster, and way more reliable.

What will we learn?

  • Write a multi-stage Dockerfile 
  • Build and push Docker image to Docker Hub
  • Write a GitHub CICD pipeline to automate the process
  • Access the cloud server and use environment variables safely
  • Use Watchtower to pull changes automatically

Note: This article assumes that you already know how to set up and write basic Nest.js applications.

Multi-stage Dockerfile

When it comes to Docker, writing a multi-stage Dockerfile is one of the smartest ways to keep your images lean and efficient. By breaking the build process into separate stages, you only include what’s necessary for production, leaving behind development tools and dependencies that aren’t needed in the final image. This not only helps keep your Docker images smaller, but it also speeds up the build process and makes your deployments more secure. In this guide, I’ll show you how to write a multi-stage Dockerfile for your NestJS app, ensuring you get the best performance and smallest possible image for production.

Let's go to the Nest.js project root directory and add a Dockerfile with the following content.

# Install dependencies
FROM node:22.14.0-alpine AS deps
WORKDIR /usr/src/app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install

# Build application
FROM node:22.14.0-alpine AS builder
WORKDIR /usr/src/app
COPY . .
COPY --from=deps /usr/src/app/node_modules ./node_modules
RUN npm install -g pnpm && pnpm build

# Production image
FROM node:22.14.0-alpine AS production
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=builder /usr/src/app/node_modules ./node_modules
CMD ["node", "dist/main.js"]

This multi-stage Dockerfile builds and runs a NestJS app efficiently in three stages:

1. Install dependencies (deps stage)

  • FROM node:22.14.0-alpine: Uses a lightweight Node.js base image.
  • WORKDIR /usr/src/app: Sets the working directory inside the container.
  • COPY package.json pnpm-lock.yaml ./: Copies dependency files.
  • RUN npm install -g pnpm && pnpm install: Installs pnpm and project dependencies.

2. Build the app (builder stage)

  • Copies the full app source (COPY . .) and dependencies from the previous stage.
  • Runs pnpm build to compile the NestJS app.

3. Create the final production image (production stage)

  • Copies only the built dist folder and node_modules needed to run the app.
  • CMD ["node", "dist/main.js"]: Defines the command to start the application.

This setup keeps the final image clean and lightweight by excluding unnecessary build files and tools.

Each stage uses its own Node.js image because Docker stages are isolated and don’t share files or environments by default.

*** Important ***

Don't forget to add .dockerignore file in the project directory to exclude sensitive and unnecessary files. If you add any sensitive information inside Docker image, anybody with the Docker image can see the environment variables. Following this practice, you will have secure and lightweight Docker images. 

Here is what it looks like;

node_modules/
dist/
*.env
*.log

Moving on to the next step:

Build and Push Docker Image to Docker Hub

Once your Dockerfile is ready, you can build and push your image to Docker Hub in just a few steps. First, make sure you're logged in to Docker Hub using docker login -u {DOCKER_USERNAME}. Then, build your image by running:

If you don't have a Docker Hub account: Follow https://hub.docker.com/ 

docker build -t {DOCKER_USERNAME}/nest-app:latest .

Replace DOCKER_USERNAME with your actual Docker Hub username and preferred image name. Once the build completes, push the image with:

docker push {DOCKER_USERNAME}/nest-app:latest

This will upload your image to Docker Hub, making it easy to pull and run from any server. Keeping your image name and tag consistent helps manage updates smoothly.

So far, so good. We’ve written a multi-stage Dockerfile for our NestJS app, built a Docker image, and pushed it to Docker Hub. But here’s the catch — every time we make changes to our application, we have to manually run the build and push commands, which adds friction to our deployment process. To solve this, we’ll use GitHub Actions to automate everything.

Write a GitHub CICD Pipeline to Automate the Process

To eliminate the need for manual Docker builds and pushes, we can set up a GitHub Actions CI/CD pipeline. This pipeline will automatically build the Docker image and push it to Docker Hub whenever we push changes to the main branch (You can choose any git branch of your choice). It’s a simple way to keep your app always ready for deployment with zero manual effort. Here is the step-by-step guide to create workflow.

1. Create the Workflow Directory

Inside your project root, create a folder for GitHub workflows:

mkdir -p .github/workflows

2. Create the Workflow File

Inside that folder, create a YAML file (e.g., docker-publish.yml):

touch .github/workflows/docker-publish.yml

3. Add CI/CD Pipeline Configuration

Open the file and paste the following workflow:

name: Build and Push Docker Image
on:
push:
branches:
- main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}

- name: Build Docker image
run: |
docker build -t ${{ secrets.DOCKER_HUB_USERNAME
}}/nest-app:latest .

- name: Push Docker image
run: |
docker push ${{secrets.DOCKER_HUB_USERNAME}}/nest-
app:latest

4. Add Docker Hub Secrets to GitHub

Go to your repo on GitHub → SettingsSecrets and variablesActions, and add:

  • DOCKER_HUB_USERNAME
  • DOCKER_HUB_TOKEN (Generate it at Docker Hub > Account Settings > Personal Access Tokens)

5. Commit and Push

Add your changes and push to GitHub:

git add .
git commit -m "Add GitHub Actions CI/CD pipeline"
git push origin main

Once pushed, the workflow will run automatically on every push to main, building and pushing your Docker image to Docker Hub. You can find the detailed logs and status of CICD on the actions tab inside your Github repository. 

We are already halfway there. Let's log in to our cloud server (VPS) and run our Nest.js app from a Docker image stored at Docker Hub.

Cloud Server and Environment Variables

1. Log in to Your Cloud Server(AWS, DigitalOcean, GCP) via SSH

Open your terminal and connect to your cloud server using its IP address:

ssh username@your-server-ip

It could be slightly different depending on your cloud server.

2. Create an .env File on the Server

Since our Docker Image does not have an access to environment variables, we can store safely in a .env file. A Better practice would be to create a project directory (mkdir my-nest-app) and then:
nano .env

Example:

PORT=5000
DATABASE_URL=your_database_url
JWT_SECRET=your_jwt_secret

Save and close the file.

3. Pull the Docker Image from Docker Hub

Run the following command to get your image:

docker pull {DOCKER_HUB_USERNAME}/nest-app:latest

Make sure it matches your Docker image name.

4. Run the Docker Container with the .env File

Use --env-file to load environment variables securely:

docker run -d --name nest-app \
--env-file .env \
-p 5000:5000 \
{DOCKER_HUB_USERNAME}/nest-app:latest

5. Check If the Container Is Running

Verify it’s up and running:

docker ps

Your app should be running at port 5000

We are 90% done here. We have everything set up, but we will still need to run commands manually to pull and run a Docker container. So, here is our final hero, Watchtower.

Watchtower to Pull Changes Automatically

Docker Watchtower is like a little helper that keeps an eye on your containers and updates them automatically whenever a new version of your image is available. Instead of manually pulling updates and restarting containers every time you push changes, Watchtower does it for you, saving time and keeping your app always up-to-date with minimal effort. It's a simple way to make your deployments more hands-free and hassle-free.

Stop and remove the existing container and re-run using watchtower like this:

docker run -d \
--name watchtower \
--restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower \
--interval 30 --cleanup nest-app

Here is the explanation of the command:

This command runs Watchtower in the background and tells it to keep an eye on your nest-app container. It checks for updates every 30 seconds (--interval 30), and if a new image is available, it automatically pulls it, restarts the container, and even removes the old image to save space (--cleanup). The -v /var/run/docker.sock:/var/run/docker.sock part lets Watchtower talk to the Docker engine so it can manage your containers.

In short: it’s a simple way to make sure your app stays updated, no manual work needed.

And that’s a wrap! You’ve just set up a full CI/CD pipeline for your NestJS app using Docker and GitHub Actions, plus added Watchtower to handle updates for you automatically. Now, every time you push changes, your app builds, ships, and runs without you lifting a finger. No more manual deploys or forgetting to update containers, it all just works. With this in place, you can focus on building features and let the pipeline handle the rest. 

Bonus:

You use the following Dockerfile if your app is using a database and Prisma ORM for development.

# Install dependencies
FROM node:22.14.0-alpine AS deps
WORKDIR /usr/src/app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install

# Build application
FROM node:22.14.0-alpine AS builder
WORKDIR /usr/src/app
COPY . .
COPY ./prisma ./prisma
COPY --from=deps /usr/src/app/node_modules ./node_modules
RUN npm install -g pnpm && pnpx prisma generate && pnpm build

# Production image
FROM node:22.14.0-alpine AS production
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/prisma ./prisma
CMD ["node", "dist/main.js"]
Happy Coding, Happy Shipping

Binod Chaudhary

devcolumn avatar

Software Engineer | Full-Stack Developer

© Copyright 2025. All Right Reserved.

Scroll to Top