Day 14: Hands-On - Building a Simple Web App with Docker
Learning Objectives: By the end of this lesson, you will be able to:
- Build a complete web application using Docker from scratch
- Integrate multiple containers with networking and data persistence
- Apply best practices for Dockerfiles, configuration, and security
- Deploy and test a full-stack application in a containerized environment
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Remember when you first learned to ride a bike? You’d practiced balance, pedaling, and steering separately, but the magic happened when you put it all together for the first time. Today, we’re taking that same approach with Docker - combining everything you’ve learned into building a real, working web application!
Lesson Overview
- Duration: 60-75 minutes
- Prerequisites: Completion of Days 8-13, understanding of web development basics
- Materials Needed: Docker installed, text editor, web browser, basic knowledge of Node.js/Express
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Project Planning - Designing our containerized web application architecture
- Database Setup - Creating a persistent PostgreSQL container
- Backend Development - Building a REST API with proper Docker integration
- Frontend Integration - Creating a web interface that connects to our API
- Full Integration - Connecting all components with networking and configuration
- Testing and Optimization - Ensuring everything works and performs well
Success Criteria:
By the end of this session, you should have a fully functional, multi-container web application that demonstrates all major Docker concepts in a real-world scenario.
3. Foundation Knowledge (10-15 minutes)
Project Architecture Overview
We’ll build a Task Management Application with the following components:
Real-World Analogy: Think of our application like a restaurant. The database is the kitchen inventory, the backend API is the kitchen staff preparing orders, and the frontend is the dining room where customers interact. Docker containers ensure each part has its own space but can communicate effectively.
Application Components:
- PostgreSQL Database: Stores tasks, users, and application data
- Backend API (Node.js/Express): Handles business logic and database operations
- Frontend (Static Web App): User interface for task management
- Reverse Proxy (Nginx): Routes requests and serves static files
Architecture Diagram
[User] → [Nginx Proxy:80] → [Frontend:3000] → [Backend API:3001] → [PostgreSQL:5432]
↓
[Static Files]
Key Concepts We’ll Apply
- Multi-container orchestration without Docker Compose (manual networking)
- Data persistence with volumes
- Environment-based configuration
- Security best practices
- Performance optimization
Project Structure
task-manager-app/
├── database/
│ ├── init.sql
│ └── Dockerfile
├── backend/
│ ├── src/
│ ├── package.json
│ ├── Dockerfile
│ └── .dockerignore
├── frontend/
│ ├── public/
│ ├── src/
│ ├── package.json
│ ├── Dockerfile
│ └── .dockerignore
├── nginx/
│ ├── nginx.conf
│ └── Dockerfile
└── docker-network-setup.sh
4. Detailed Explanation and Examples (20-25 minutes)
Database Layer Design
PostgreSQL Schema
-- init.sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending',
priority VARCHAR(10) DEFAULT 'medium',
user_id INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample data
INSERT INTO users (username, email) VALUES
('john_doe', 'john@example.com'),
('jane_smith', 'jane@example.com')
ON CONFLICT (username) DO NOTHING;
INSERT INTO tasks (title, description, status, priority, user_id) VALUES
('Setup Docker Environment', 'Install and configure Docker for development', 'completed', 'high', 1),
('Learn Container Networking', 'Study Docker networking concepts', 'in_progress', 'medium', 1),
('Build Web Application', 'Create a full-stack app with Docker', 'pending', 'high', 2)
ON CONFLICT DO NOTHING;
Database Dockerfile
FROM postgres:13-alpine
# Copy initialization script
COPY init.sql /docker-entrypoint-initdb.d/
# Set default environment variables
ENV POSTGRES_DB=taskmanager
ENV POSTGRES_USER=taskuser
ENV POSTGRES_PASSWORD=taskpass
# Create volume for data persistence
VOLUME ["/var/lib/postgresql/data"]
EXPOSE 5432
Backend API Design
Express.js Application Structure
// backend/src/app.js
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
const app = express();
// Configuration from environment variables
const config = {
port: process.env.PORT || 3001,
database: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'taskmanager',
user: process.env.DB_USER || 'taskuser',
password: process.env.DB_PASSWORD || 'taskpass'
}
};
// Database connection
const pool = new Pool(config.database);
// Middleware
app.use(cors());
app.use(express.json());
// Health check endpoint
app.get('/health', async (req, res) => {
try {
await pool.query('SELECT 1');
res.json({ status: 'healthy', database: 'connected' });
} catch (error) {
res.status(500).json({ status: 'unhealthy', error: error.message });
}
});
// API Routes
app.get('/api/tasks', async (req, res) => {
try {
const result = await pool.query(`
SELECT t.*, u.username
FROM tasks t
JOIN users u ON t.user_id = u.id
ORDER BY t.created_at DESC
`);
res.json(result.rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/tasks', async (req, res) => {
const { title, description, priority, user_id } = req.body;
try {
const result = await pool.query(
'INSERT INTO tasks (title, description, priority, user_id) VALUES ($1, $2, $3, $4) RETURNING *',
[title, description, priority, user_id]
);
res.status(201).json(result.rows[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.put('/api/tasks/:id', async (req, res) => {
const { id } = req.params;
const { title, description, status, priority } = req.body;
try {
const result = await pool.query(
'UPDATE tasks SET title = $1, description = $2, status = $3, priority = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
[title, description, status, priority, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Task not found' });
}
res.json(result.rows[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.delete('/api/tasks/:id', async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query('DELETE FROM tasks WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ message: 'Task deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/users', async (req, res) => {
try {
const result = await pool.query('SELECT id, username, email FROM users ORDER BY username');
res.json(result.rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(config.port, () => {
console.log(`Backend API running on port ${config.port}`);
console.log('Database config:', {
host: config.database.host,
database: config.database.database,
user: config.database.user
});
});
Backend Dockerfile
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeuser -u 1001
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy application source
COPY src/ ./src/
# Change ownership to non-root user
RUN chown -R nodeuser:nodejs /app
USER nodeuser
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "
const http = require('http');
const options = { host: 'localhost', port: 3001, path: '/health', timeout: 2000 };
const req = http.request(options, (res) => {
if (res.statusCode === 200) process.exit(0);
else process.exit(1);
});
req.on('error', () => process.exit(1));
req.on('timeout', () => process.exit(1));
req.end();
"
CMD ["node", "src/app.js"]
Frontend Design
Static Web Application
<!-- frontend/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager - Docker Demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.task-card {
transition: transform 0.2s;
}
.task-card:hover {
transform: translateY(-2px);
}
.priority-high { border-left: 4px solid #dc3545; }
.priority-medium { border-left: 4px solid #ffc107; }
.priority-low { border-left: 4px solid #28a745; }
.status-completed { opacity: 0.7; }
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-tasks"></i> Task Manager
<small class="text-muted">Docker Demo Application</small>
</h1>
</div>
</div>
<!-- Add Task Form -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Add New Task</h5>
</div>
<div class="card-body">
<form id="taskForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="taskTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="taskTitle" required>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="taskPriority" class="form-label">Priority</label>
<select class="form-select" id="taskPriority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="taskUser" class="form-label">Assign to</label>
<select class="form-select" id="taskUser" required>
<option value="">Select user...</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="taskDescription" class="form-label">Description</label>
<textarea class="form-control" id="taskDescription" rows="2"></textarea>
</div>
<button type="submit" class="btn btn-primary">Add Task</button>
</form>
</div>
</div>
</div>
</div>
<!-- Tasks Display -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5>Tasks</h5>
<button class="btn btn-outline-secondary btn-sm" onclick="loadTasks()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
<div id="tasksContainer">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://kit.fontawesome.com/your-fontawesome-kit.js" crossorigin="anonymous"></script>
<script src="app.js"></script>
</body>
</html>
// frontend/public/app.js
class TaskManager {
constructor() {
this.apiBase = '/api';
this.tasks = [];
this.users = [];
this.init();
}
async init() {
await this.loadUsers();
await this.loadTasks();
this.setupEventListeners();
}
async loadUsers() {
try {
const response = await fetch(`${this.apiBase}/users`);
this.users = await response.json();
this.populateUserSelect();
} catch (error) {
this.showError('Failed to load users');
console.error('Error loading users:', error);
}
}
async loadTasks() {
try {
const response = await fetch(`${this.apiBase}/tasks`);
this.tasks = await response.json();
this.renderTasks();
} catch (error) {
this.showError('Failed to load tasks');
console.error('Error loading tasks:', error);
}
}
async createTask(taskData) {
try {
const response = await fetch(`${this.apiBase}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(taskData)
});
if (response.ok) {
await this.loadTasks();
this.clearForm();
this.showSuccess('Task created successfully!');
} else {
this.showError('Failed to create task');
}
} catch (error) {
this.showError('Failed to create task');
console.error('Error creating task:', error);
}
}
async updateTaskStatus(taskId, newStatus) {
try {
const task = this.tasks.find(t => t.id === taskId);
const response = await fetch(`${this.apiBase}/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...task,
status: newStatus
})
});
if (response.ok) {
await this.loadTasks();
this.showSuccess('Task updated successfully!');
} else {
this.showError('Failed to update task');
}
} catch (error) {
this.showError('Failed to update task');
console.error('Error updating task:', error);
}
}
async deleteTask(taskId) {
if (!confirm('Are you sure you want to delete this task?')) return;
try {
const response = await fetch(`${this.apiBase}/tasks/${taskId}`, {
method: 'DELETE'
});
if (response.ok) {
await this.loadTasks();
this.showSuccess('Task deleted successfully!');
} else {
this.showError('Failed to delete task');
}
} catch (error) {
this.showError('Failed to delete task');
console.error('Error deleting task:', error);
}
}
populateUserSelect() {
const select = document.getElementById('taskUser');
select.innerHTML = '<option value="">Select user...</option>';
this.users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.username;
select.appendChild(option);
});
}
renderTasks() {
const container = document.getElementById('tasksContainer');
if (this.tasks.length === 0) {
container.innerHTML = `
<div class="text-center text-muted">
<p>No tasks found. Add your first task above!</p>
</div>
`;
return;
}
const tasksHtml = this.tasks.map(task => `
<div class="card task-card mb-3 priority-${task.priority} ${task.status === 'completed' ? 'status-completed' : ''}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="card-title">
${task.title}
<span class="badge bg-${this.getPriorityColor(task.priority)} ms-2">${task.priority}</span>
<span class="badge bg-${this.getStatusColor(task.status)} ms-1">${task.status}</span>
</h6>
<p class="card-text text-muted">${task.description || 'No description'}</p>
<small class="text-muted">
Assigned to: ${task.username} |
Created: ${new Date(task.created_at).toLocaleDateString()}
</small>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
Actions
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="taskManager.updateTaskStatus(${task.id}, 'pending')">Mark Pending</a></li>
<li><a class="dropdown-item" href="#" onclick="taskManager.updateTaskStatus(${task.id}, 'in_progress')">Mark In Progress</a></li>
<li><a class="dropdown-item" href="#" onclick="taskManager.updateTaskStatus(${task.id}, 'completed')">Mark Completed</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" onclick="taskManager.deleteTask(${task.id})">Delete</a></li>
</ul>
</div>
</div>
</div>
</div>
`).join('');
container.innerHTML = tasksHtml;
}
getPriorityColor(priority) {
const colors = { high: 'danger', medium: 'warning', low: 'success' };
return colors[priority] || 'secondary';
}
getStatusColor(status) {
const colors = { pending: 'secondary', in_progress: 'primary', completed: 'success' };
return colors[status] || 'secondary';
}
setupEventListeners() {
document.getElementById('taskForm').addEventListener('submit', (e) => {
e.preventDefault();
const taskData = {
title: document.getElementById('taskTitle').value,
description: document.getElementById('taskDescription').value,
priority: document.getElementById('taskPriority').value,
user_id: parseInt(document.getElementById('taskUser').value)
};
this.createTask(taskData);
});
}
clearForm() {
document.getElementById('taskForm').reset();
}
showError(message) {
this.showAlert(message, 'danger');
}
showSuccess(message) {
this.showAlert(message, 'success');
}
showAlert(message, type) {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.querySelector('.container').insertAdjacentHTML('afterbegin', alertHtml);
setTimeout(() => {
const alert = document.querySelector('.alert');
if (alert) alert.remove();
}, 5000);
}
}
// Initialize the application
const taskManager = new TaskManager();
5. Live Coding Demonstration (30-35 minutes)
Step 1: Project Setup and Database Container
# Create project structure
mkdir task-manager-app
cd task-manager-app
# Create directories
mkdir -p database backend/src frontend/public nginx
# Create Docker network for our application
docker network create task-manager-net
# Create volume for database persistence
docker volume create task-manager-db
# Create database initialization script
cat > database/init.sql << 'EOF'
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending',
priority VARCHAR(10) DEFAULT 'medium',
user_id INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, email) VALUES
('john_doe', 'john@example.com'),
('jane_smith', 'jane@example.com')
ON CONFLICT (username) DO NOTHING;
INSERT INTO tasks (title, description, status, priority, user_id) VALUES
('Setup Docker Environment', 'Install and configure Docker for development', 'completed', 'high', 1),
('Learn Container Networking', 'Study Docker networking concepts', 'in_progress', 'medium', 1),
('Build Web Application', 'Create a full-stack app with Docker', 'pending', 'high', 2)
ON CONFLICT DO NOTHING;
EOF
# Start PostgreSQL container
docker run -d \
--name task-db \
--network task-manager-net \
-v task-manager-db:/var/lib/postgresql/data \
-v $(pwd)/database/init.sql:/docker-entrypoint-initdb.d/init.sql \
-e POSTGRES_DB=taskmanager \
-e POSTGRES_USER=taskuser \
-e POSTGRES_PASSWORD=taskpass \
postgres:13-alpine
# Wait for database to start
echo "Waiting for database to initialize..."
sleep 10
# Test database connectivity
docker exec task-db psql -U taskuser -d taskmanager -c "SELECT COUNT(*) FROM tasks;"
Step 2: Backend API Development
# Create package.json for backend
cat > backend/package.json << 'EOF'
{
"name": "task-manager-backend",
"version": "1.0.0",
"description": "Task Manager API",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.8.0",
"cors": "^2.8.5"
},
"engines": {
"node": ">=18.0.0"
}
}
EOF
# Create .dockerignore for backend
cat > backend/.dockerignore << 'EOF'
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
coverage
.nyc_output
EOF
# Build backend image
docker build -t task-manager-backend backend/
# Run backend container
docker run -d \
--name task-api \
--network task-manager-net \
-p 3001:3001 \
-e DB_HOST=task-db \
-e DB_NAME=taskmanager \
-e DB_USER=taskuser \
-e DB_PASSWORD=taskpass \
task-manager-backend
# Test API health
curl http://localhost:3001/health
# Test API endpoints
curl http://localhost:3001/api/tasks
curl http://localhost:3001/api/users
Step 3: Frontend Development and Integration
# Create package.json for frontend (for build tools if needed)
cat > frontend/package.json << 'EOF'
{
"name": "task-manager-frontend",
"version": "1.0.0",
"description": "Task Manager Frontend",
"scripts": {
"serve": "http-server public -p 3000"
}
}
EOF
# Create Dockerfile for frontend
cat > frontend/Dockerfile << 'EOF'
FROM nginx:alpine
# Copy static files
COPY public/ /usr/share/nginx/html/
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
# Create nginx config for frontend
cat > frontend/nginx.conf << 'EOF'
server {
listen 80;
server_name localhost;
# Serve static files
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Proxy API requests to backend
location /api/ {
proxy_pass http://task-api:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
# Build frontend image
docker build -t task-manager-frontend frontend/
# Run frontend container
docker run -d \
--name task-frontend \
--network task-manager-net \
-p 8080:80 \
task-manager-frontend
# Test the complete application
echo "Application available at: http://localhost:8080"
Step 4: Testing and Verification
# Check all containers are running
docker ps --filter network=task-manager-net
# Test database connectivity
docker exec task-db psql -U taskuser -d taskmanager -c "SELECT title, status FROM tasks;"
# Test API endpoints
echo "Testing API Health:"
curl -s http://localhost:3001/health | jq
echo "Testing Tasks Endpoint:"
curl -s http://localhost:3001/api/tasks | jq
echo "Testing Users Endpoint:"
curl -s http://localhost:3001/api/users | jq
# Test frontend serving
echo "Testing Frontend:"
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080
# Check container logs
echo "Backend logs:"
docker logs task-api --tail 5
echo "Frontend logs:"
docker logs task-frontend --tail 5
Step 5: Adding New Task via API
# Add a new task via API
curl -X POST http://localhost:3001/api/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Test Docker Integration",
"description": "Verify that all containers work together properly",
"priority": "high",
"user_id": 1
}'
# Update task status
TASK_ID=$(curl -s http://localhost:3001/api/tasks | jq '.[-1].id')
curl -X PUT http://localhost:3001/api/tasks/$TASK_ID \
-H "Content-Type: application/json" \
-d '{
"title": "Test Docker Integration",
"description": "Verify that all containers work together properly",
"status": "completed",
"priority": "high"
}'
# Verify changes
curl -s http://localhost:3001/api/tasks | jq '.[] | select(.title | contains("Test Docker"))'
6. Hands-On Practice (25-30 minutes)
Exercise 1: Add User Management Feature (15 minutes)
Objective: Extend the application with user creation functionality
Requirements:
- Add POST endpoint for creating new users in the backend
- Add user registration form in the frontend
- Implement user validation (unique username/email)
- Update the task assignment dropdown dynamically
Backend API Extension:
// Add to backend/src/app.js
app.post('/api/users', async (req, res) => {
const { username, email } = req.body;
// Validation
if (!username || !email) {
return res.status(400).json({ error: 'Username and email are required' });
}
try {
const result = await pool.query(
'INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id, username, email, created_at',
[username, email]
);
res.status(201).json(result.rows[0]);
} catch (error) {
if (error.code === '23505') { // Unique violation
res.status(409).json({ error: 'Username or email already exists' });
} else {
res.status(500).json({ error: error.message });
}
}
});
Frontend Extension Task: Add user registration form and integrate with the backend API.
Exercise 2: Add Task Filtering and Search (10 minutes)
Objective: Implement client-side task filtering
Requirements:
- Add filter buttons for task status (all, pending, in_progress, completed)
- Add search input for task titles
- Add priority filter dropdown
- Update the task display logic to handle filters
Implementation Guide:
// Add to frontend/public/app.js
filterTasks(filters = {}) {
let filteredTasks = [...this.tasks];
if (filters.status && filters.status !== 'all') {
filteredTasks = filteredTasks.filter(task => task.status === filters.status);
}
if (filters.priority && filters.priority !== 'all') {
filteredTasks = filteredTasks.filter(task => task.priority === filters.priority);
}
if (filters.search) {
const searchLower = filters.search.toLowerCase();
filteredTasks = filteredTasks.filter(task =>
task.title.toLowerCase().includes(searchLower) ||
(task.description && task.description.toLowerCase().includes(searchLower))
);
}
this.renderTasks(filteredTasks);
}
Exercise 3: Performance Optimization and Monitoring (Optional)
Objective: Add performance monitoring and optimization
Tasks:
- Add response time logging to the backend
- Implement database connection pooling optimization
- Add frontend loading states and error boundaries
- Create a monitoring dashboard endpoint
Monitoring Endpoint Example:
// Add to backend
app.get('/api/stats', async (req, res) => {
try {
const taskStats = await pool.query(`
SELECT
status,
COUNT(*) as count,
priority,
COUNT(*) FILTER (WHERE priority = 'high') as high_priority_count
FROM tasks
GROUP BY status, priority
`);
const userCount = await pool.query('SELECT COUNT(*) FROM users');
res.json({
tasks: taskStats.rows,
total_users: parseInt(userCount.rows[0].count),
database_connection: 'healthy'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Issues and Solutions
- Problem: Containers can’t communicate with each other Cause: Not using the same Docker network Solution: Ensure all containers are on the same custom network
# Check container networks
docker inspect task-api | grep -A 5 "Networks"
docker inspect task-db | grep -A 5 "Networks"
# Connect container to network if needed
docker network connect task-manager-net task-api
- Problem: Database connection fails Cause: Database container not ready or wrong connection parameters Solution: Add health checks and proper startup ordering
# Check database logs
docker logs task-db
# Test database connectivity
docker exec task-api ping task-db
docker exec task-db pg_isready -U taskuser -d taskmanager
- Problem: Frontend can’t reach backend API Cause: CORS issues or incorrect proxy configuration Solution: Configure CORS properly and check nginx proxy settings
// In backend app.js, configure CORS properly
app.use(cors({
origin: ['http://localhost:8080', 'http://localhost:3000'],
credentials: true
}));
- Problem: Data doesn’t persist after container restart Cause: Volume not properly mounted Solution: Verify volume mounting and check data location
# Check volume mounting
docker inspect task-db | grep -A 10 "Mounts"
# Check data persistence
docker exec task-db ls -la /var/lib/postgresql/data
Debugging Tools and Commands
# Container status overview
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# Network connectivity test
docker exec task-frontend ping task-api
docker exec task-api ping task-db
# Check container resource usage
docker stats --no-stream
# View application logs
docker logs -f task-api
docker logs -f task-frontend
docker logs -f task-db
# Inspect container configuration
docker inspect task-api | jq '.Config.Env'
docker inspect task-api | jq '.NetworkSettings'
8. Best Practices and Professional Tips (10 minutes)
Production Readiness Checklist
Security Hardening
# Run containers as non-root user
docker run --user 1001:1001 task-manager-backend
# Use specific image tags
FROM node:18.17.0-alpine # Instead of node:18-alpine
# Remove unnecessary packages and files
RUN apk del .build-deps && \
rm -rf /var/cache/apk/* /tmp/*
Performance Optimization
# Multi-stage build for smaller images
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
USER node
CMD ["node", "src/app.js"]
Monitoring and Logging
# Add structured logging
docker run \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
task-manager-backend
# Health check implementation
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1
Environment Management
# Use environment-specific configurations
# development.env
cat > development.env << EOF
NODE_ENV=development
DB_HOST=localhost
LOG_LEVEL=debug
ENABLE_DEBUG_ROUTES=true
EOF
# production.env
cat > production.env << EOF
NODE_ENV=production
DB_HOST=prod-db.company.com
LOG_LEVEL=warn
ENABLE_DEBUG_ROUTES=false
EOF
Deployment Strategies
# Blue-green deployment simulation
docker run -d --name task-api-blue task-manager-backend:v1
docker run -d --name task-api-green task-manager-backend:v2
# Rolling update pattern
docker stop task-api-old
docker run -d --name task-api-new task-manager-backend:latest
docker rm task-api-old
Next Steps for Production
- Implement proper secret management
- Add SSL/TLS termination
- Set up database backups and replication
- Implement monitoring and alerting
- Add automated testing pipelines
- Learn Docker Compose for easier orchestration
9. Summary and Review (5 minutes)
Key Takeaways
- Integration is key - Docker’s real power comes from connecting multiple containers effectively
- Network design matters - Custom networks enable secure, scalable communication
- Data persistence requires planning - Volumes and proper database setup are crucial
- Configuration flexibility enables portability - Environment variables make containers adaptable
Application Architecture Achieved
✅ Database Layer: PostgreSQL with persistent data storage
✅ Backend API: RESTful service with proper error handling
✅ Frontend Interface: Responsive web application
✅ Service Integration: All components communicating securely
✅ Development Workflow: Easy to modify and redeploy
Quick Review Questions
- How would you add a new service to this application stack?
- What steps would you take to deploy this to production?
- How would you handle database migrations and updates?
Sample Answers:
- Create new container, connect to task-manager-net, update nginx proxy if needed
- Use production environment variables, implement SSL, set up monitoring, use specific image tags
- Create migration scripts, use init containers, implement versioned database schema updates
For Instructors
Teaching Tips
- Start with simple containers and gradually add complexity
- Emphasize the connection between containers throughout the demo
- Show both working and broken states to highlight important concepts
- Use real browser testing to make the application tangible
Assessment Ideas
- Have students extend the application with new features
- Create deployment scenarios with different requirements
- Troubleshooting exercises with intentionally broken configurations
- Performance optimization challenges
Adaptation Notes
- For beginners: Focus more on the step-by-step process and basic concepts
- For advanced students: Introduce more complex architectures and production concerns
- Online delivery: Ensure students can follow along with their own environments
- Time management: This is a comprehensive lesson - consider splitting into multiple sessions if needed
Common Student Questions
- “Why not use Docker Compose?” - Great question! We’ll learn that tomorrow, but understanding manual networking helps you debug Compose issues
- “How do I deploy this to the cloud?” - We’ll cover cloud deployment in later lessons
- “What about HTTPS and security?” - Excellent thinking! We’ll address production security in advanced lessons