NestJS course Β· Module 12: Containers and CI/CD

Dockerfile for NestJS - Building the Camp

4 min read
In this lesson5

Legionary, it is time to learn how to write camp construction instructions - that is, a Dockerfile! A Dockerfile is a text file containing a sequence of commands that Docker executes to build an image. Each command creates a new layer (stratum) in the image.

Basic Dockerfile Instructions

A Dockerfile consists of instructions, each serving a specific role:

FROM - Foundation of the Camp

The FROM instruction selects the base image on which we build. For NestJS applications we use the Node.js image:

1# We use the official Node.js image in Alpine version (lightweight!)
2FROM node:20-alpine

Alpine Linux is a minimalist distribution - it weighs ~5MB instead of ~900MB for a full Ubuntu. It is like a light field camp instead of a full fortress - quick to set up and move.

WORKDIR - Designating the Construction Site

The WORKDIR instruction sets the working directory inside the container:

1# Set the working directory
2WORKDIR /app

COPY - Transporting Materials

COPY copies files from our computer into the container:

1# Copy package.json and yarn.lock files
2COPY package.json yarn.lock ./
3
4# Copy the rest of the source code
5COPY . .

RUN - Executing Construction Work

RUN executes commands during image build:

1# Install dependencies
2RUN yarn install --frozen-lockfile
3
4# Build the application
5RUN yarn build

CMD - Startup Order

CMD defines the command to run when the container starts:

1# Run the application
2CMD ["node", "dist/main"]

Simple Dockerfile for NestJS

1FROM node:20-alpine
2
3WORKDIR /app
4
5COPY package.json yarn.lock ./
6RUN yarn install --frozen-lockfile
7
8COPY . .
9RUN yarn build
10
11EXPOSE 4000
12
13CMD ["node", "dist/main"]

Multi-stage Build - Advanced Build Strategy

A simple Dockerfile has a problem - the final image contains development tools, TypeScript source files, and devDependencies. It is like leaving scaffolding after the fortress construction is complete.

Multi-stage build allows using multiple build stages, where the final image contains only what is needed to run:

1# =========================
2# STAGE 1: Builder (Legion Engineers)
3# =========================
4FROM node:20-alpine AS builder
5
6WORKDIR /app
7
8# Copy dependency files
9COPY package.json yarn.lock ./
10RUN yarn install --frozen-lockfile
11
12# Copy source code and build
13COPY . .
14RUN yarn build
15
16# Install only production dependencies
17RUN yarn install --frozen-lockfile --production
18
19# =========================
20# STAGE 2: Production (Ready Camp)
21# =========================
22FROM node:20-alpine AS production
23
24WORKDIR /app
25
26# Copy ONLY the built code and production dependencies
27COPY --from=builder /app/dist ./dist
28COPY --from=builder /app/node_modules ./node_modules
29COPY --from=builder /app/package.json ./package.json
30
31# Create a non-root user
32RUN addgroup -g 1001 -S nodejs && \
33    adduser -S nestjs -u 1001
34
35USER nestjs
36
37EXPOSE 4000
38
39CMD ["node", "dist/main"]

Size Comparison

1const comparison = {
2  singleStage: {
3    size: '~800 MB',
4    contains: ['node_modules (dev + prod)', 'src/', 'test/', '.git'],
5    security: 'root user'
6  },
7  multiStage: {
8    size: '~200 MB',
9    contains: ['dist/', 'node_modules (prod only)', 'package.json'],
10    security: 'non-root user (nestjs)'
11  }
12};

The .dockerignore File

Just as .gitignore prevents adding files to the repository, .dockerignore prevents copying unnecessary files into the image:

1node_modules
2dist
3.git
4.gitignore
5*.md
6.env
7.env.*
8test
9coverage
10.vscode
11.idea

Thanks to .dockerignore, the COPY . . command will not copy these files, speeding up the build and reducing image size.

Building and Running

1# Build the image
2docker build -t roman-imperium-api:1.0 .
3
4# Run the container
5docker run -d -p 4000:4000 --name imperium-api roman-imperium-api:1.0
6
7# Check logs
8docker logs imperium-api
9
10# Verify it works
11curl http://localhost:4000/health

Remember, legionary - multi-stage build is the best practice for production. We build heavy, but deploy light!

Code for this lesson: src/dockerfile-example.ts
1// Dockerfile - Instructions for building a legion camp
2console.log("=== DOCKERFILE FOR NestJS ===\n");
3
4// Dockerfile instructions
5interface DockerInstruction {
6  command: string;
7  purpose: string;
8  example: string;
9}
10
11const instructions: DockerInstruction[] = [
12  {
13    command: 'FROM',
14    purpose: 'Base image (foundation of the camp)',
15    example: 'FROM node:20-alpine',
16  },
17  {
18    command: 'WORKDIR',
19    purpose: 'Working directory (construction site)',
20    example: 'WORKDIR /app',
21  },
22  {
23    command: 'COPY',
24    purpose: 'Copying files (transporting materials)',
25    example: 'COPY package.json yarn.lock ./',
26  },
27  {
28    command: 'RUN',
29    purpose: 'Executing a command (construction work)',
30    example: 'RUN yarn install --frozen-lockfile',
31  },
32  {
33    command: 'EXPOSE',
34    purpose: 'Port declaration (camp gate)',
35    example: 'EXPOSE 4000',
36  },
37  {
38    command: 'CMD',
39    purpose: 'Startup command (the order to start)',
40    example: 'CMD ["node", "dist/main"]',
41  },
42];
43
44console.log("Dockerfile instructions:\n");
45instructions.forEach(i => {
46  console.log(`${i.command}: ${i.purpose}`);
47  console.log(`  Example: ${i.example}\n`);
48});
49
50// Multi-stage build comparison
51console.log("=== MULTI-STAGE BUILD ===\n");
52console.log("Single-stage: ~800 MB (dev + prod dependencies)");
53console.log("Multi-stage:  ~200 MB (only production)\n");
54
55console.log("Stage 1 (builder): install, build, compile");
56console.log("Stage 2 (production): COPY --from=builder only dist/");
57console.log("\nBenefit: smaller image, no dev-dependencies, non-root user");
58

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the FROM instruction define in a Dockerfile?

  2. 2. What is the main benefit of multi-stage build in Docker?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Define two Dockerfile stages: builder (building) and production (running)

  • Click in order

    Arrange the Dockerfile instructions in the correct order:

  • Code editor

    Select which files/folders should be ignored by Docker (node_modules, dist, .git, .env, src, test, package.json, coverage)

  • Horizontal ordering

    Arrange the elements of the Docker image build command in the correct order:

Useful articles