Shipping Your Code with Confidence
"But it works on my machine!"
Your application works perfectly on your laptop. But when you deploy it to a server, or a new developer joins the team, it crashes.
Why? The environment is different.
This inconsistency is a massive source of bugs, lost time, and frustration.
A VM emulates an entire computer, including a full guest operating system. This provides total isolation.
Analogy: If you need a place to live, you build a brand new, separate house with its own foundation, plumbing, and electrical systems.
Problem: VMs are huge (gigabytes), slow to boot, and resource-heavy. Running several VMs on one machine is very inefficient.
A container packages up your application code along with all its dependencies, but it shares the host operating system's kernel.
Analogy: If you need a place to live, you move into an apartment. You have your own private space, but you share the building's foundation, plumbing, and electrical systems.
Benefit: Containers are tiny (megabytes), boot in seconds, and are incredibly efficient.
Installing the Engine
For macOS and Windows, the easiest way to get started is with Docker Desktop.
On Linux, you install the Docker Engine directly.
For Ubuntu/Debian:
# Update package index and install prerequisites
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Set up the repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Once installed, open a new terminal and run this command to verify everything is working:
docker run hello-world
You should see a message starting with "Hello from Docker!" This confirms your installation is successful.
The Blueprint for Your Container
A `Dockerfile` is a simple text file that contains a list of instructions for how to build a Docker image.
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install app dependencies
RUN npm install
# Copy the rest of your app's source code
COPY . .
# Make port 3000 available to the world outside this container
EXPOSE 3000
# Define the command to run your app
CMD [ "node", "src/main.js" ]
Similar to `.gitignore`, a `.dockerignore` file tells Docker which files and folders to exclude when copying files into the image.
You should ALWAYS ignore `node_modules` and other local artifacts.
# .dockerignore
node_modules
npm-debug.log
dist
.git
.env
Now we use the Docker CLI to build the image and run the container.
# Build the image from the Dockerfile in the current directory
# The -t flag "tags" (names) the image
docker build -t my-nest-app .
# Run a container from the image
# The -p flag "publishes" a port, mapping host port 3000 to container port 3000
docker run -p 3000:3000 my-nest-app
Multi-Stage Builds for Security & Size
Our simple `Dockerfile` has two major problems for production:
A multi-stage build uses multiple `FROM` instructions in a single `Dockerfile`. Each `FROM` begins a new "stage". You can selectively copy artifacts from one stage to another, leaving behind everything you don't need.
# ---- Stage 1: Build ----
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
# Install all dependencies, including devDependencies
RUN npm install
COPY . .
# Build the production JavaScript files
RUN npm run build
# ---- Stage 2: Production ----
FROM node:18-alpine
WORKDIR /app
# Only copy the production dependencies manifest
COPY package*.json ./
# Install ONLY production dependencies
RUN npm install --omit=dev
# Copy the compiled code from the 'builder' stage
COPY --from=builder /app/dist ./dist
EXPOSE 3000
# Run the compiled JavaScript code
CMD [ "node", "dist/main.js" ]
Your task is to take the NestJS application you've been building and write a complete, multi-stage `Dockerfile` to containerize it.