Containerise a Node.js Web Application with Docker
Build your first Docker container by packaging a Node.js Express app, writing a Dockerfile, and running it locally with Docker Compose.
Domains & Technologies
Blueprint Walkthrough
Architecture Overview
This project teaches you the most fundamental skill in modern DevOps — packaging an application into a Docker container. You will take a simple Node.js web server, write a Dockerfile that describes how to build the container image, and run it using Docker Compose.
Every DevOps engineer uses Docker daily. Before you can deploy to Kubernetes, ECS, or any cloud platform, you need to understand how containers work from the ground up. This project builds that foundation.
Your Node.js App (code files) | v [ Dockerfile ] (instructions to build the image) | v [ Docker Image ] (snapshot of your app + dependencies) | v [ Docker Container ] (running instance of the image) | vhttp://localhost:3000(your app is live!)Problem Solved
Before Docker, the most common problem in software teams was — it works on my laptop but not on the server. Each developer had slightly different versions of Node.js, different operating systems, and different environment configurations. The server had a different setup again.
Docker solves this by packaging your application together with everything it needs — the exact Node.js version, all npm dependencies, and environment variables — into a single portable image. That image runs identically on your laptop, your colleague's laptop, and the production server. No more environment mismatch.
Step-by-Step Implementation Guide
Step 1: Install Docker and Create the Application
First install Docker Desktop from the official website. Docker Desktop includes everything you need — the Docker Engine, Docker Compose, and a GUI to see your containers.
## Verify Docker is installed correctlydocker --version## Expected: Docker version 24.x.x or higher docker compose version## Expected: Docker Compose version v2.x.x or higherCreate your project directory and a simple Node.js application:
mkdir my-docker-app && cd my-docker-appCreate package.json:
{ "name": "my-docker-app", "version": "1.0.0", "main": "src/index.js", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js" }, "dependencies": { "express": "^4.18.2" }}Create src/index.js:
const express = require('express');const app = express();const PORT = process.env.PORT || 3000; // Health check endpoint// Docker and Kubernetes use this to verify the app is aliveapp.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: process.env.APP_VERSION || '1.0.0' });}); // Main endpointapp.get('/', (req, res) => { const name = process.env.APP_NAME || 'DevOps App'; res.json({ message: `Hello from ${name}!`, environment: process.env.NODE_ENV || 'development' });}); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`);});RememberThe
/healthendpoint is not just for testing. In production, Kubernetes, ECS, and load balancers all call this endpoint automatically to check if your container is alive. Always include it.
Step 2: Write Your First Dockerfile
A Dockerfile is a text file with instructions that Docker reads top to bottom to build your image. Think of it as a recipe — each line adds a layer to the image.
Create Dockerfile in your project root:
## Stage 1: Start from an official Node.js base image## alpine = lightweight Linux (only 5MB vs 900MB for full Ubuntu)FROM node:20-alpine ## Set the working directory inside the container## All subsequent commands run from this directoryWORKDIR /app ## Copy package files FIRST (before copying source code)## Why? Docker caches each layer. If package.json does not change,## Docker reuses the cached npm install layer — much faster buildsCOPY package*.json ./ ## Install dependencies inside the containerRUN npm install --only=production ## Now copy your source code## This layer changes every time you edit codeCOPY src/ ./src/ ## Document which port the app listens on## This is documentation only — does not actually expose the portEXPOSE 3000 ## Security: run as non-root user## The node user comes pre-created in the official node imageUSER node ## The command to start the applicationCMD ["node", "src/index.js"]Create .dockerignore to exclude files from the image:
node_modules.git.gitignore*.logREADME.mdTipThe
.dockerignorefile works exactly like.gitignore. Always addnode_moduleshere — you never want to copy your localnode_modulesinto the image. TheRUN npm installstep inside Docker installs them fresh for the container's operating system.
Step 3: Build and Run Your Docker Image
## Build the image and tag it with a name## The dot at the end means 'use the Dockerfile in the current directory'docker build -t my-docker-app:v1 . ## Watch the build output — you will see each layer being processed## Expected output includes:## => [1/5] FROM node:20-alpine## => [2/5] WORKDIR /app## => [3/5] COPY package*.json ./## => [4/5] RUN npm install## => [5/5] COPY src/ ./src/## => exporting to image ## List your imagesdocker images | grep my-docker-app## Expected: my-docker-app v1 abc123 2 minutes ago ~150MB ## Run the container## -d = detached (run in background)## -p 3000:3000 = map port 3000 on your machine to port 3000 in container## --name = give it a readable namedocker run -d -p 3000:3000 --name webapp my-docker-app:v1 ## Verify it is runningdocker ps## Expected: webapp container showing status Up ## Test the applicationcurl http://localhost:3000## Expected: {"message":"Hello from DevOps App!","environment":"development"} curl http://localhost:3000/health## Expected: {"status":"healthy","timestamp":"...","version":"1.0.0"}Step 4: Add Docker Compose for Multi-Container Setup
Docker Compose lets you define and run multiple containers together. You will add a Redis container alongside your app — this is how real applications work in production.
Create docker-compose.yml:
version: '3.8' services: # Your Node.js application webapp: build: . # Build from the Dockerfile in current directory ports: - "3000:3000" environment: - NODE_ENV=development - APP_NAME=DevOps Network App - APP_VERSION=1.0.0 - REDIS_URL=redis://redis:6379 depends_on: - redis # Start redis before the webapp restart: unless-stopped # Redis cache container # Notice: no Dockerfile needed — we use the official image directly redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis-data:/data # Persist Redis data between restarts restart: unless-stopped ## Named volume for Redis persistencevolumes: redis-data:## Stop the container you ran manuallydocker stop webapp && docker rm webapp ## Start everything with Docker Composedocker compose up -d ## Check both containers are runningdocker compose ps## Expected: webapp and redis both showing status running ## View logs from all containersdocker compose logs ## View logs from just the webapp, follow in real timedocker compose logs -f webapp ## Test the app is still workingcurl http://localhost:3000RememberIn Docker Compose, containers communicate using their service names as hostnames. Your webapp reaches Redis at
redis://redis:6379— the hostnameredisresolves to the Redis container automatically. This is Docker's built-in DNS.
Step 5: Understand Image Layers and Caching
This step teaches you the most important Docker optimisation — layer caching. Run these commands and observe how much faster the second build is.
## Make a small change to your source codeecho "// updated" >> src/index.js ## Rebuild the image — time ittime docker build -t my-docker-app:v2 . ## Watch the output carefully:## => CACHED [2/5] WORKDIR /app <- reused from cache## => CACHED [3/5] COPY package*.json ./ <- reused from cache## => CACHED [4/5] RUN npm install <- reused from cache (no package change)## => [5/5] COPY src/ ./src/ <- rebuilt (source code changed) ## Now change package.json (add a new dependency)npm install --save lodash ## Rebuild and observetime docker build -t my-docker-app:v3 .## This time npm install runs again because package.json changed## The layers after the change are all rebuilt## This is why we copy package.json BEFORE source code in the DockerfileCommon MistakeBeginners often write the Dockerfile like this: copy all files first, then run npm install. This means EVERY code change invalidates the npm install cache and npm install runs every single build. Always copy
package*.jsonand runnpm installBEFORE copying your source code.
Step 6: Push to Docker Hub
Docker Hub is the public registry for Docker images — like GitHub for code, but for container images.
## Create a free account at hub.docker.com## Then log in from your terminaldocker login## Enter your Docker Hub username and password ## Tag your image with your Docker Hub username## Format: username/image-name:tagdocker tag my-docker-app:v1 YOUR_DOCKERHUB_USERNAME/my-docker-app:v1docker tag my-docker-app:v1 YOUR_DOCKERHUB_USERNAME/my-docker-app:latest ## Push to Docker Hubdocker push YOUR_DOCKERHUB_USERNAME/my-docker-app:v1docker push YOUR_DOCKERHUB_USERNAME/my-docker-app:latest ## Verify — pull the image from Docker Hub to confirm it was uploadeddocker rmi YOUR_DOCKERHUB_USERNAME/my-docker-app:v1docker pull YOUR_DOCKERHUB_USERNAME/my-docker-app:v1docker run -d -p 3001:3000 YOUR_DOCKERHUB_USERNAME/my-docker-app:v1curl http://localhost:3001## Expected: Same response — image runs identically from Docker HubValidation & Testing
## 1. Verify both containers are runningdocker compose ps## Expected: webapp (running) and redis (running) ## 2. Test the health endpointcurl http://localhost:3000/health## Expected: {"status":"healthy",...} ## 3. Test environment variable injectiondocker run --rm \ -e APP_NAME="Zerodha Backend" \ -e APP_VERSION="2.5.0" \ -p 3001:3000 \ my-docker-app:v1curl http://localhost:3001## Expected: {"message":"Hello from Zerodha Backend!", ...}## Different output from same image — environment variables change behaviour ## 4. Check image size is reasonabledocker images my-docker-app:v1## Expected: Size around 150-200MB (alpine keeps it small) ## 5. Verify non-root userdocker run --rm my-docker-app:v1 whoami## Expected: node (not root!) ## 6. Verify .dockerignore is workingdocker run --rm my-docker-app:v1 ls /app## Expected: package.json, package-lock.json, src/## node_modules should NOT be present — installed fresh inside ## 7. Inspect image layersdocker history my-docker-app:v1## Expected: Shows each layer with its size## The npm install layer should be the largest ## 8. Clean up everythingdocker compose down -v## Expected: Stops containers and removes the redis-data volumeecho "Docker project complete!"Videos & Guides
Docker Official Documentation — Getting Started
Official Docker getting started guide covering installation, building your first image, running containers, and Docker Compose fundamentals.
Dockerfile Best Practices — Official Guide
Official Docker documentation for Dockerfile best practices including layer caching, multi-stage builds, security, and image size optimisation.
Docker Tutorial for Beginners — TechWorld with Nana
Complete Docker beginner tutorial covering containers vs VMs, Dockerfile, Docker Compose, image layers, and pushing to Docker Hub with practical examples.