NestJS container deployment
Simplify Your Node.js/Nest.js Deployment: Step-by-Step Guide with Docker and Google Cloud
A corrected guide to multi-stage Node.js and NestJS containers, immutable artifacts, and choosing between Docker Hub and Cloud Build for Cloud Run delivery.
8 min readBy Abdullah Raheel
Deploying a Node.js or NestJS service
This is the deployment path I used in 2023 for a NestJS microservice: build a Docker image, publish it from GitHub Actions, and deploy it to Cloud Run. I have corrected the parts of the original guide that I would not ship today.
Docker packages the runtime, dependencies, and application into one image. It reduces differences between local, CI, and Cloud Run environments, but configuration and infrastructure can still differ.
This walkthrough covers three stages: building and testing the image, publishing it through GitHub Actions and Docker Hub, and deploying it to Google Cloud with either manual revisions or continuous deployment.
Prerequisites
- Install Docker before starting the build.
- On Windows, install WSL (Windows Subsystem for Linux) so Docker can use the required Linux environment.
- If WSL installation fails with error 0x80370114, enable the required virtualization features before continuing.
Stage 1: Create the Docker image
The original 2023 guide used one Node 18 Alpine stage, installed production dependencies, and then ran the Nest build in that same image. That ordering is not a dependable NestJS build: TypeScript and other build tools commonly live in devDependencies. The corrected image separates compilation from the runtime so the build has its full toolchain without shipping that toolchain to production.
FROM node:18-alpine AS build
WORKDIR /usr/src/app
# Copy manifests first so dependency layers can be reused.
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /usr/src/app/dist ./dist
EXPOSE 8080
USER node
CMD ["node", "dist/main.js"]The build stage uses npm ci, which requires a committed lockfile, and compiles with the project-pinned Nest and TypeScript versions. The runtime stage receives production dependencies and compiled output, then drops root privileges with the image's node user.
Dockerfile
.dockerignore
node_modules
npm-debug.log
distThe ignored files keep the Docker build context smaller and prevent host dependencies or an existing dist directory from being copied into the image.
Run and inspect the container locally
Build the image and confirm that Docker created it.
docker build --tag nest-microservice .
docker imagesStart a detached container and map its internal port 8080 to local port 5000. Check docker ps, call the HTTP health endpoint, and inspect docker logs before publishing the image.
docker run -d --publish 5000:8080 --name nest-container nest-microservice
docker ps
docker ps --all
docker stop nest-containerAfter the local image and container work, push Dockerfile and .dockerignore to the GitHub repository.
Stage 2: Publish the image with GitHub Actions
Continuous integration requires a GitHub repository containing the Node.js or NestJS source and a Docker Hub account. Create a Docker Hub personal access token, then store the account name and token as repository secrets named DOCKERHUB_USERNAME and DOCKERHUB_TOKEN.
The 2023 workflow below ran on every push to main, checked out the source, signed in to Docker Hub, enabled Buildx, built the root Dockerfile, and pushed abdullahraheel/nest-microservice:latest. It is retained as the original implementation, including its historical action versions.
name: ci
on:
push:
branches:
- "main"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/nest-microservice:latest- Save and commit the workflow.
- Push the change to main so the workflow starts.
- Inspect the Actions run and diagnose the exact step if a job fails.
- Verify that the published repository and tag appear in Docker Hub.
Stage 3: Deploy the image to Google Cloud Run
Create a Google Cloud project, open Cloud Run, and create a service. For a manual deployment, provide the Docker Hub image as username/repository:tag, choose the region and service settings, and create the revision. The example image is abdullahraheel/nest-microservice:latest; use the exact repository and tag produced by your own workflow.
Connect Cloud Build for continuous deployment
- Choose Create Service and select the option to set up continuous deployment with Cloud Build.
- Select the repository provider and repository.
- Choose the deployment branch, using main in this example.
- Select Dockerfile as the build type and provide its path.
- Save the configuration and create the service.
Once the connection is active, changes merged into main trigger Cloud Build to build the repository and redeploy the service automatically. This is an alternative to deploying the Docker Hub image produced in Stage 2, not another consumer of that same artifact.
Summary and references
- Build the NestJS or Node.js project as a Docker image and test its container locally.
- Choose one delivery path: publish a versioned image through GitHub Actions, or let Cloud Build build the connected repository.
- Deploy an exact image revision manually, or use the connected Cloud Build path for automatic service revisions.
- Use an external scheduler or singleton control for scheduled work; a Cloud Run web instance is not inherently a single durable cron worker.

