Hands-on Lab

#Lab: Multi-Container Application with Docker Compose

Build a full-stack application using Docker Compose.

#๐ŸŽฏ Objectives

  • Create a multi-service application
  • Configure service dependencies
  • Manage data with volumes
  • Set up container networking

#๐Ÿ“‹ Prerequisites

  • Docker and Docker Compose installed
  • Basic Docker knowledge

#โฑ๏ธ Duration: 45 minutes


#Task 1: Project Setup (5 min)

bash
mkdir ~/docker-fullstack && cd ~/docker-fullstack

Create directory structure:

bash
mkdir -p backend frontend

#Task 2: Create Backend (Node.js API) (10 min)

#backend/package.json

bash
1cat << 'EOF' > backend/package.json
2{
3  "name": "api",
4  "version": "1.0.0",
5  "main": "server.js",
6  "dependencies": {
7    "express": "^4.18.2",
8    "pg": "^8.11.3"
9  }
10}
11EOF

#backend/server.js

bash
1cat << 'EOF' > backend/server.js
2const express = require('express');
3const { Pool } = require('pg');
4
5const app = express();
6app.use(express.json());
7
8const pool = new Pool({
9  host: process.env.DB_HOST || 'db',
10  user: process.env.DB_USER || 'postgres',
11  password: process.env.DB_PASSWORD || 'secret',
12  database: process.env.DB_NAME || 'app'
13});
14
15app.get('/health', (req, res) => {
16  res.json({ status: 'healthy', timestamp: new Date() });
17});
18
19app.get('/api/items', async (req, res) => {
20  try {
21    const result = await pool.query('SELECT * FROM items ORDER BY id');
22    res.json(result.rows);
23  } catch (err) {
24    res.status(500).json({ error: err.message });
25  }
26});
27
28app.post('/api/items', async (req, res) => {
29  const { name } = req.body;
30  try {
31    const result = await pool.query(
32      'INSERT INTO items (name) VALUES ($1) RETURNING *',
33      [name]
34    );
35    res.status(201).json(result.rows[0]);
36  } catch (err) {
37    res.status(500).json({ error: err.message });
38  }
39});
40
41const PORT = process.env.PORT || 3000;
42app.listen(PORT, () => {
43  console.log(`API running on port ${PORT}`);
44});
45EOF

#backend/Dockerfile

bash
1cat << 'EOF' > backend/Dockerfile
2FROM node:20-alpine
3WORKDIR /app
4COPY package*.json ./
5RUN npm install
6COPY . .
7EXPOSE 3000
8CMD ["node", "server.js"]
9EOF

#Task 3: Create Frontend (10 min)

#frontend/index.html

bash
1cat << 'EOF' > frontend/index.html
2<!DOCTYPE html>
3<html lang="en">
4<head>
5  <meta charset="UTF-8">
6  <title>Docker Fullstack Demo</title>
7  <style>
8    body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
9    input { padding: 10px; width: 300px; }
10    button { padding: 10px 20px; cursor: pointer; }
11    ul { list-style: none; padding: 0; }
12    li { padding: 10px; background: #f0f0f0; margin: 5px 0; border-radius: 5px; }
13    .status { padding: 10px; border-radius: 5px; margin-bottom: 20px; }
14    .healthy { background: #d4edda; }
15    .error { background: #f8d7da; }
16  </style>
17</head>
18<body>
19  <h1>Docker Fullstack Demo</h1>
20  <div id="status" class="status">Checking...</div>
21  
22  <h2>Add Item</h2>
23  <input type="text" id="itemName" placeholder="Enter item name">
24  <button onclick="addItem()">Add</button>
25  
26  <h2>Items</h2>
27  <ul id="items"></ul>
28
29  <script>
30    async function checkHealth() {
31      try {
32        const res = await fetch('/api/health');
33        const data = await res.json();
34        document.getElementById('status').textContent = 'API Status: ' + data.status;
35        document.getElementById('status').className = 'status healthy';
36      } catch (err) {
37        document.getElementById('status').textContent = 'API Error: ' + err.message;
38        document.getElementById('status').className = 'status error';
39      }
40    }
41
42    async function loadItems() {
43      try {
44        const res = await fetch('/api/items');
45        const items = await res.json();
46        document.getElementById('items').innerHTML = 
47          items.map(i => '<li>' + i.name + '</li>').join('');
48      } catch (err) {
49        console.error(err);
50      }
51    }
52
53    async function addItem() {
54      const name = document.getElementById('itemName').value;
55      if (!name) return;
56      try {
57        await fetch('/api/items', {
58          method: 'POST',
59          headers: { 'Content-Type': 'application/json' },
60          body: JSON.stringify({ name })
61        });
62        document.getElementById('itemName').value = '';
63        loadItems();
64      } catch (err) {
65        console.error(err);
66      }
67    }
68
69    checkHealth();
70    loadItems();
71  </script>
72</body>
73</html>
74EOF

#frontend/nginx.conf

bash
1cat << 'EOF' > frontend/nginx.conf
2server {
3    listen 80;
4    location / {
5        root /usr/share/nginx/html;
6        try_files $uri $uri/ /index.html;
7    }
8    location /api {
9        proxy_pass http://backend:3000;
10        proxy_set_header Host $host;
11    }
12}
13EOF

#frontend/Dockerfile

bash
1cat << 'EOF' > frontend/Dockerfile
2FROM nginx:alpine
3COPY nginx.conf /etc/nginx/conf.d/default.conf
4COPY index.html /usr/share/nginx/html/
5EXPOSE 80
6EOF

#Task 4: Create Docker Compose (10 min)

#docker-compose.yml

bash
1cat << 'EOF' > docker-compose.yml
2version: '3.8'
3
4services:
5  db:
6    image: postgres:15-alpine
7    environment:
8      POSTGRES_USER: postgres
9      POSTGRES_PASSWORD: secret
10      POSTGRES_DB: app
11    volumes:
12      - postgres_data:/var/lib/postgresql/data
13      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
14    healthcheck:
15      test: ["CMD-SHELL", "pg_isready -U postgres"]
16      interval: 5s
17      timeout: 5s
18      retries: 5
19
20  backend:
21    build: ./backend
22    environment:
23      DB_HOST: db
24      DB_USER: postgres
25      DB_PASSWORD: secret
26      DB_NAME: app
27    depends_on:
28      db:
29        condition: service_healthy
30    restart: unless-stopped
31
32  frontend:
33    build: ./frontend
34    ports:
35      - "8080:80"
36    depends_on:
37      - backend
38    restart: unless-stopped
39
40volumes:
41  postgres_data:
42EOF

#Database Initialization

bash
1cat << 'EOF' > init.sql
2CREATE TABLE IF NOT EXISTS items (
3  id SERIAL PRIMARY KEY,
4  name VARCHAR(255) NOT NULL,
5  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
6);
7
8INSERT INTO items (name) VALUES ('Sample Item 1'), ('Sample Item 2');
9EOF

#Task 5: Run and Test (10 min)

bash
1# Build and start all services
2docker compose up -d --build
3
4# Check status
5docker compose ps
6
7# View logs
8docker compose logs -f
9
10# Test API
11curl http://localhost:8080/api/health
12curl http://localhost:8080/api/items
13
14# Open in browser
15echo "Open http://localhost:8080 in browser"

#โœ… Success Criteria

  • All three services running
  • Health endpoint returns status
  • Items can be added and listed
  • Data persists across restarts

#๐Ÿงน Cleanup

bash
docker compose down -v
cd ~ && rm -rf docker-fullstack