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.
A Dockerfile consists of instructions, each serving a specific role:
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-alpineAlpine 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.
The
WORKDIR instruction sets the working directory inside the container:1# Set the working directory
2WORKDIR /appCOPY 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 executes commands during image build:1# Install dependencies
2RUN yarn install --frozen-lockfile
3
4# Build the application
5RUN yarn buildCMD defines the command to run when the container starts:1# Run the application
2CMD ["node", "dist/main"]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"]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 /app/dist ./dist
28COPY /app/node_modules ./node_modules
29COPY /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"]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};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.ideaThanks to
.dockerignore, the COPY . . command will not copy these files, speeding up the build and reducing image size.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/healthRemember, legionary — multi-stage build is the best practice for production. We build heavy, but deploy light!