hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Deploying Node.js Apps with Docker

Why Docker

  • The "Works on My Machine" Problem
  • Docker Concepts

Your First Dockerfile

  • Writing a Dockerfile
  • Multi-Stage Builds
  • The .dockerignore File

Running Containers

  • Running and Managing Containers
  • Environment Variables and Secrets
  • Health Checks

Multi-Container Apps

  • Docker Compose
  • Adding a Reverse Proxy
  • Persistent Data

Deploying to Production

  • Deploying to a VPS
  • HTTPS with Let's Encrypt
  • Zero-Downtime Deploys

CI/CD

  • Building Images in CI
  • Automated Deployment

Production Hardening

  • Container Security
  • Logging and Monitoring
  • Deployment Checklist and Capstone

Writing a Dockerfile

The Dockerfile

A Dockerfile is a text file with instructions for building an image. Each instruction creates a layer. The file lives in your project root.

A simple Dockerfile

For a Hectoday HTTP app:

# Dockerfile
FROM node:22-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy dependency files first (for caching)
COPY package.json package-lock.json ./

# Install production dependencies only
RUN npm ci --omit=dev

# Copy the rest of the application code
COPY . .

# The port the app listens on
EXPOSE 3000

# Start the app
CMD ["node", "src/server.js"]

What each instruction does

FROM node:22-alpine — Start from the Node.js 22 Alpine image. This gives us Node.js and a minimal Linux system.

WORKDIR /app — All subsequent commands run inside /app. Like cd /app but it creates the directory if it does not exist.

COPY package.json package-lock.json ./ — Copy only the dependency files first. This is a caching optimization — dependencies change less often than code.

RUN npm ci --omit=dev — Install dependencies. npm ci is faster and more deterministic than npm install (it uses the lockfile exactly). --omit=dev skips devDependencies (TypeScript, @types packages) because they are not needed in production.

COPY . . — Copy everything else (your source code). This layer changes every time you change your code, but the dependency layer above is cached.

EXPOSE 3000 — Documents which port the app uses. This is informational — it does not actually publish the port. You do that with docker run -p.

CMD ["node", "src/server.js"] — The command that runs when the container starts. Use the exec form (JSON array) not the shell form (string). The exec form runs Node.js directly as PID 1 (the main process in the container), which means it receives shutdown signals (like SIGTERM when Docker stops the container) directly. If you use the shell form (CMD node src/server.js), a shell process becomes PID 1 and Node.js does not receive the signal — your app does not shut down gracefully.

Building the image

docker build -t myapp .

-t myapp names (tags) the image. . is the build context (the current directory — Docker sends these files to the build engine).

Step 1/7 : FROM node:22-alpine
 ---> Using cache
Step 2/7 : WORKDIR /app
 ---> Using cache
Step 3/7 : COPY package.json package-lock.json ./
 ---> Using cache
Step 4/7 : RUN npm ci --omit=dev
 ---> Using cache      ← dependencies cached!
Step 5/7 : COPY . .
 ---> abc123def456      ← code changed, this layer rebuilds
Step 6/7 : EXPOSE 3000
Step 7/7 : CMD ["node", "src/server.js"]
Successfully built abc123def456
Successfully tagged myapp:latest

Notice “Using cache” on the dependency steps. Only the COPY . . step rebuilds because only the code changed. This is why we copy package.json separately.

Running the image

docker run -p 3000:3000 myapp

-p 3000:3000 maps port 3000 on your machine to port 3000 in the container. Open http://localhost:3000/health — you should see {"status":"ok"}.

The TypeScript problem

The Dockerfile above runs node src/server.js, but your source code is TypeScript (.ts). You have two options:

Option A: Use tsx in the container. Install tsx and run with it. Simple but adds overhead and dev dependencies to the image.

Option B: Compile TypeScript, copy only JavaScript. Build in a separate stage, copy the compiled output. This is the multi-stage build approach — the next lesson.

Exercises

Exercise 1: Create the Dockerfile. Build the image with docker build -t myapp .. Verify it builds successfully.

Exercise 2: Run the container with docker run -p 3000:3000 myapp. Open http://localhost:3000/health in your browser.

Exercise 3: Change your application code (add a console.log). Rebuild. Notice that the dependency layer says “Using cache” while the code layer rebuilds.

Why do we COPY package.json before COPY . .?

← Docker Concepts Multi-Stage Builds →

© 2026 hectoday. All rights reserved.