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) | v http://localhost:3000 (your app is live!)
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 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. ```bash ## Verify Docker is installed correctly docker --version ## Expected: Docker version 24.x.x or higher docker compose version ## Expected: Docker Compose version v2.x.x or higher ``` Create your project directory and a simple Node.js application: ```bash mkdir my-docker-app && cd my-docker-app ``` Create `package.json`: ```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`: ```javascript 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 alive app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: process.env.APP_VERSION || '1.0.0' }); }); // Main endpoint app.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}`); }); ``` > 📌 **Remember:** The `/health` endpoint 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: ```dockerfile ## 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 directory WORKDIR /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 builds COPY package*.json ./ ## Install dependencies inside the container RUN npm install --only=production ## Now copy your source code ## This layer changes every time you edit code COPY src/ ./src/ ## Document which port the app listens on ## This is documentation only — does not actually expose the port EXPOSE 3000 ## Security: run as non-root user ## The node user comes pre-created in the official node image USER node ## The command to start the application CMD ["node", "src/index.js"] ``` Create `.dockerignore` to exclude files from the image: ```text node_modules .git .gitignore *.log README.md ``` > 💡 **Tip:** The `.dockerignore` file works exactly like `.gitignore`. Always add `node_modules` here — you never want to copy your local `node_modules` into the image. The `RUN npm install` step inside Docker installs them fresh for the container's operating system. ### Step 3: Build and Run Your Docker Image ```bash ## 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 images docker 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 name docker run -d -p 3000:3000 --name webapp my-docker-app:v1 ## Verify it is running docker ps ## Expected: webapp container showing status Up ## Test the application curl 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`: ```yaml 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 persistence volumes: redis-data: ``` ```bash ## Stop the container you ran manually docker stop webapp && docker rm webapp ## Start everything with Docker Compose docker compose up -d ## Check both containers are running docker compose ps ## Expected: webapp and redis both showing status running ## View logs from all containers docker compose logs ## View logs from just the webapp, follow in real time docker compose logs -f webapp ## Test the app is still working curl http://localhost:3000 ``` > 📌 **Remember:** In Docker Compose, containers communicate using their service names as hostnames. Your webapp reaches Redis at `redis://redis:6379` — the hostname `redis` resolves 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. ```bash ## Make a small change to your source code echo "// updated" >> src/index.js ## Rebuild the image — time it time 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 observe time 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 Dockerfile ``` > 🔴 **Common Mistake:** Beginners 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*.json` and run `npm install` BEFORE 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. ```bash ## Create a free account at hub.docker.com ## Then log in from your terminal docker login ## Enter your Docker Hub username and password ## Tag your image with your Docker Hub username ## Format: username/image-name:tag docker tag my-docker-app:v1 YOUR_DOCKERHUB_USERNAME/my-docker-app:v1 docker tag my-docker-app:v1 YOUR_DOCKERHUB_USERNAME/my-docker-app:latest ## Push to Docker Hub docker push YOUR_DOCKERHUB_USERNAME/my-docker-app:v1 docker push YOUR_DOCKERHUB_USERNAME/my-docker-app:latest ## Verify — pull the image from Docker Hub to confirm it was uploaded docker rmi YOUR_DOCKERHUB_USERNAME/my-docker-app:v1 docker pull YOUR_DOCKERHUB_USERNAME/my-docker-app:v1 docker run -d -p 3001:3000 YOUR_DOCKERHUB_USERNAME/my-docker-app:v1 curl http://localhost:3001 ## Expected: Same response — image runs identically from Docker Hub ```
```bash ## 1. Verify both containers are running docker compose ps ## Expected: webapp (running) and redis (running) ## 2. Test the health endpoint curl http://localhost:3000/health ## Expected: {"status":"healthy",...} ## 3. Test environment variable injection docker run --rm \ -e APP_NAME="Zerodha Backend" \ -e APP_VERSION="2.5.0" \ -p 3001:3000 \ my-docker-app:v1 curl http://localhost:3001 ## Expected: {"message":"Hello from Zerodha Backend!", ...} ## Different output from same image — environment variables change behaviour ## 4. Check image size is reasonable docker images my-docker-app:v1 ## Expected: Size around 150-200MB (alpine keeps it small) ## 5. Verify non-root user docker run --rm my-docker-app:v1 whoami ## Expected: node (not root!) ## 6. Verify .dockerignore is working docker 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 layers docker history my-docker-app:v1 ## Expected: Shows each layer with its size ## The npm install layer should be the largest ## 8. Clean up everything docker compose down -v ## Expected: Stops containers and removes the redis-data volume echo "Docker project complete!" ```
This project teaches you the most fundamental skill in modern DevOps — packaging an application into a Docker container....
Before Docker, the most common problem in software teams was — it works on my laptop but not on the server. Each develop...
Step 1: Install Docker and Create the Application First install Docker Desktop from the official website. Docker Desktop...
...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.