Day 7: Hands-On - Running Your First Containerized App
Learning Objectives: By the end of this lesson, you will be able to:
- Build and deploy a complete containerized web application from scratch
- Integrate multiple Docker concepts into a cohesive project
- Troubleshoot common containerization issues in real applications
- Implement proper container lifecycle management for development workflows
- Apply Docker best practices in a practical, real-world scenario
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Imagine you’re opening a new restaurant. You’ve learned about ingredients (images), cooking techniques (commands), and recipe organization (Docker Hub). Today, you’re going to prepare and serve your first complete meal - putting everything together to create a working containerized application that real users could access!
Lesson Overview
- Duration: 75-90 minutes
- Prerequisites: All previous Docker lessons (Days 1-6)
- Materials Needed: Docker installed, text editor, web browser, all previous knowledge
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Project Planning - Designing a containerized application architecture
- Application Development - Creating a real web application with backend and frontend
- Containerization - Writing production-ready Dockerfiles
- Integration - Connecting multiple services and managing data
- Deployment - Running the complete application stack
- Testing and Validation - Ensuring everything works correctly
Success Criteria:
By the end of this session, you should have a fully functional containerized web application running on your machine, demonstrating mastery of fundamental Docker concepts.
3. Foundation Knowledge (15-20 minutes)
What Makes a Complete Containerized Application?
A real-world containerized application typically consists of multiple components working together:
- Frontend: User interface (web page, mobile app interface)
- Backend API: Business logic and data processing
- Database: Data persistence layer
- Configuration: Environment-specific settings
- Networking: Communication between components
- Storage: File and data management
Real-World Analogy: Think of a containerized application like a complete restaurant operation. The frontend is the dining room where customers interact, the backend is the kitchen where food is prepared, the database is the pantry where ingredients are stored, and networking is the wait staff connecting everything together.
Why This Matters: Understanding how to orchestrate multiple containers prepares you for real-world scenarios where applications are composed of multiple services working together.
Application Architecture Patterns
Monolithic Containerization
- Single container with all application components
- Simpler to start with and deploy
- Good for learning and small applications
Microservices Architecture
- Multiple containers, each with specific responsibilities
- More complex but more scalable and maintainable
- Industry standard for large applications
Hybrid Approach
- Logical separation within containers
- Balance between simplicity and modularity
- Good stepping stone to microservices
Key Concepts for Today’s Project
- Container Communication: How containers talk to each other
- Data Persistence: Ensuring data survives container restarts
- Environment Configuration: Managing settings across different environments
- Port Management: Exposing services for external access
- Error Handling: Graceful failure and recovery strategies
4. Detailed Explanation and Examples (25-30 minutes)
Today’s Project: Personal Task Manager
We’ll build a complete personal task manager application with the following features:
Application Features
- Frontend: Modern web interface for managing tasks
- Backend API: REST API for task operations (CRUD)
- Database: Persistent storage for tasks
- Real-time Updates: Dynamic UI updates
- Data Validation: Input validation and error handling
Technical Stack
- Frontend: HTML, CSS, JavaScript (vanilla or simple framework)
- Backend: Python Flask or Node.js Express
- Database: SQLite (simple) or PostgreSQL (advanced)
- Container: Docker with proper networking and volumes
Architecture Overview
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web Browser │───▶│ Frontend │───▶│ Backend API │
│ (Port 3000) │ │ (Static Files) │ │ (Port 5000) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Database │
│ (Volume) │
└─────────────────┘
Development Workflow Strategy
- Start Simple: Begin with a working application outside containers
- Containerize Components: Move each part into containers systematically
- Connect Services: Establish communication between containers
- Add Persistence: Implement data storage with volumes
- Optimize and Polish: Improve performance and user experience
Container Design Principles
Single Responsibility
Each container should have one primary responsibility:
- Frontend container serves static files
- Backend container handles API requests
- Database container manages data storage
Configuration Management
- Use environment variables for configuration
- Separate development and production settings
- Avoid hardcoding values in application code
Data Management
- Use volumes for persistent data
- Separate application data from container lifecycle
- Implement backup and recovery strategies
5. Live Coding Demonstration (35-40 minutes)
Step 1: Create the Backend API
Let’s start by creating a simple Flask API:
# Create project structure
mkdir task-manager-app
cd task-manager-app
mkdir backend frontend
# Create the backend application
cd backend
Create the Flask application:
# backend/app.py
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import sqlite3
import os
from datetime import datetime
app = Flask(__name__)
CORS(app) # Enable Cross-Origin Resource Sharing
# Database setup
DATABASE = '/data/tasks.db'
def init_db():
"""Initialize the database with tasks table"""
os.makedirs(os.path.dirname(DATABASE), exist_ok=True)
conn = sqlite3.connect(DATABASE)
conn.execute('''
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def get_db():
"""Get database connection"""
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row # Enable column access by name
return conn
@app.route('/api/tasks', methods=['GET'])
def get_tasks():
"""Get all tasks"""
conn = get_db()
tasks = conn.execute('SELECT * FROM tasks ORDER BY created_at DESC').fetchall()
conn.close()
return jsonify([dict(task) for task in tasks])
@app.route('/api/tasks', methods=['POST'])
def create_task():
"""Create a new task"""
data = request.json
if not data or not data.get('title'):
return jsonify({'error': 'Title is required'}), 400
conn = get_db()
cursor = conn.execute(
'INSERT INTO tasks (title, description) VALUES (?, ?)',
(data['title'], data.get('description', ''))
)
task_id = cursor.lastrowid
conn.commit()
conn.close()
return jsonify({'id': task_id, 'message': 'Task created successfully'}), 201
@app.route('/api/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
"""Update a task"""
data = request.json
conn = get_db()
conn.execute(
'UPDATE tasks SET title = ?, description = ?, completed = ? WHERE id = ?',
(data.get('title'), data.get('description'), data.get('completed'), task_id)
)
conn.commit()
if conn.total_changes == 0:
conn.close()
return jsonify({'error': 'Task not found'}), 404
conn.close()
return jsonify({'message': 'Task updated successfully'})
@app.route('/api/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
"""Delete a task"""
conn = get_db()
conn.execute('DELETE FROM tasks WHERE id = ?', (task_id,))
conn.commit()
if conn.total_changes == 0:
conn.close()
return jsonify({'error': 'Task not found'}), 404
conn.close()
return jsonify({'message': 'Task deleted successfully'})
@app.route('/health')
def health_check():
"""Health check endpoint"""
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5000, debug=True)
Create requirements file:
# backend/requirements.txt
Flask==2.3.2
Flask-CORS==4.0.0
Step 2: Create the Frontend
# Move to frontend directory
cd ../frontend
Create the HTML interface:
<!-- frontend/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</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>📝 Personal Task Manager</h1>
<p>Manage your tasks efficiently with Docker!</p>
</header>
<section class="task-form">
<h2>Add New Task</h2>
<form id="taskForm">
<input type="text" id="taskTitle" placeholder="Task title *" required>
<textarea id="taskDescription" placeholder="Task description (optional)"></textarea>
<button type="submit">Add Task</button>
</form>
</section>
<section class="task-list">
<h2>Your Tasks</h2>
<div id="tasksContainer">
<p class="loading">Loading tasks...</p>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
Create the CSS styling:
/* frontend/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.task-form, .task-list {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.task-form h2, .task-list h2 {
color: #4a5568;
margin-bottom: 20px;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
}
#taskForm {
display: flex;
flex-direction: column;
gap: 15px;
}
input, textarea {
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
button {
background: #667eea;
color: white;
padding: 12px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background: #5a6fd8;
}
.task-item {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 5px;
padding: 15px;
margin-bottom: 10px;
transition: all 0.3s;
}
.task-item:hover {
transform: translateY(-2px);
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.task-item.completed {
opacity: 0.7;
text-decoration: line-through;
}
.task-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
.task-actions button {
padding: 5px 10px;
font-size: 14px;
}
.btn-complete {
background: #38a169;
}
.btn-delete {
background: #e53e3e;
}
.loading {
text-align: center;
color: #718096;
font-style: italic;
}
.error {
color: #e53e3e;
background: #fed7d7;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
@media (max-width: 600px) {
.container {
padding: 10px;
}
header h1 {
font-size: 2em;
}
.task-form, .task-list {
padding: 15px;
}
}
Create the JavaScript functionality:
// frontend/script.js
class TaskManager {
constructor() {
this.apiUrl = 'http://localhost:5000/api';
this.tasksContainer = document.getElementById('tasksContainer');
this.taskForm = document.getElementById('taskForm');
this.init();
}
init() {
this.taskForm.addEventListener('submit', this.handleSubmit.bind(this));
this.loadTasks();
}
async handleSubmit(e) {
e.preventDefault();
const title = document.getElementById('taskTitle').value.trim();
const description = document.getElementById('taskDescription').value.trim();
if (!title) {
this.showError('Please enter a task title');
return;
}
try {
await this.createTask({ title, description });
this.taskForm.reset();
this.loadTasks();
} catch (error) {
this.showError('Failed to create task: ' + error.message);
}
}
async createTask(taskData) {
const response = await fetch(`${this.apiUrl}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(taskData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async loadTasks() {
try {
this.tasksContainer.innerHTML = '<p class="loading">Loading tasks...</p>';
const response = await fetch(`${this.apiUrl}/tasks`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const tasks = await response.json();
this.renderTasks(tasks);
} catch (error) {
this.showError('Failed to load tasks: ' + error.message);
}
}
renderTasks(tasks) {
if (tasks.length === 0) {
this.tasksContainer.innerHTML = '<p class="loading">No tasks yet. Add your first task above!</p>';
return;
}
this.tasksContainer.innerHTML = tasks.map(task => `
<div class="task-item ${task.completed ? 'completed' : ''}">
<h3>${this.escapeHtml(task.title)}</h3>
${task.description ? `<p>${this.escapeHtml(task.description)}</p>` : ''}
<small>Created: ${new Date(task.created_at).toLocaleString()}</small>
<div class="task-actions">
<button class="btn-complete" onclick="taskManager.toggleTask(${task.id}, ${!task.completed})">
${task.completed ? 'Mark Incomplete' : 'Mark Complete'}
</button>
<button class="btn-delete" onclick="taskManager.deleteTask(${task.id})">
Delete
</button>
</div>
</div>
`).join('');
}
async toggleTask(taskId, completed) {
try {
const response = await fetch(`${this.apiUrl}/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ completed }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.loadTasks();
} catch (error) {
this.showError('Failed to update task: ' + error.message);
}
}
async deleteTask(taskId) {
if (!confirm('Are you sure you want to delete this task?')) {
return;
}
try {
const response = await fetch(`${this.apiUrl}/tasks/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.loadTasks();
} catch (error) {
this.showError('Failed to delete task: ' + error.message);
}
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = message;
this.tasksContainer.insertBefore(errorDiv, this.tasksContainer.firstChild);
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
}
// Initialize the task manager when the page loads
const taskManager = new TaskManager();
Step 3: Create Dockerfiles
Backend Dockerfile:
# backend/Dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app.py .
# Create volume mount point for database
RUN mkdir -p /data
# Expose port
EXPOSE 5000
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
# Run the application
CMD ["python", "app.py"]
Frontend Dockerfile:
# frontend/Dockerfile
FROM nginx:alpine
# Copy static files to nginx directory
COPY . /usr/share/nginx/html/
# Copy custom nginx configuration if needed
# COPY nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# Nginx runs in foreground by default in containers
CMD ["nginx", "-g", "daemon off;"]
Step 4: Build and Run the Application
# Build the backend image
cd backend
docker build -t task-manager-backend:1.0 .
# Build the frontend image
cd ../frontend
docker build -t task-manager-frontend:1.0 .
# Create a network for the containers
docker network create task-network
# Create a volume for database persistence
docker volume create task-data
# Run the backend
docker run -d \
--name task-backend \
--network task-network \
-p 5000:5000 \
-v task-data:/data \
task-manager-backend:1.0
# Run the frontend
docker run -d \
--name task-frontend \
--network task-network \
-p 3000:80 \
task-manager-frontend:1.0
# Check that both containers are running
docker ps
Explanation:
- We create a custom network so containers can communicate
- We use a volume to persist database data
- Backend runs on port 5000, frontend on port 3000
- Both containers are connected to the same network
6. Hands-On Practice (25-30 minutes)
Exercise 1: Test and Validate the Application (10 minutes)
Objective: Ensure the containerized application works correctly
Instructions:
- Access the application at http://localhost:3000
- Create several tasks with different titles and descriptions
- Mark some tasks as complete
- Delete some tasks
- Restart the containers and verify data persistence
Testing Checklist:
- Frontend loads successfully
- Can create new tasks
- Can mark tasks as complete/incomplete
- Can delete tasks
- Data persists after container restart
- API health check responds correctly
Exercise 2: Troubleshooting Challenge (10 minutes)
Objective: Practice debugging containerized applications
Challenge: Intentionally break something and then fix it:
- Stop the backend container
- Try to create a task (should fail)
- Check frontend error handling
- Restart the backend and verify recovery
Advanced Debugging:
# Check container logs
docker logs task-backend
docker logs task-frontend
# Check container health
docker inspect task-backend | grep -A 5 "Health"
# Test API directly
curl http://localhost:5000/health
curl http://localhost:5000/api/tasks
# Check network connectivity
docker exec task-frontend ping task-backend
Exercise 3: Enhancement and Customization (10 minutes)
Objective: Practice modifying containerized applications
Enhancement Ideas:
- Add a timestamp display to tasks
- Implement task categories or tags
- Add a search/filter functionality
- Change the color scheme or styling
Implementation Process:
- Modify the frontend code
- Rebuild the frontend image
- Stop and remove the old frontend container
- Run the new frontend container
- Test the changes
Example Enhancement:
# Stop and remove old frontend
docker stop task-frontend
docker rm task-frontend
# Rebuild with changes
docker build -t task-manager-frontend:1.1 .
# Run updated version
docker run -d \
--name task-frontend \
--network task-network \
-p 3000:80 \
task-manager-frontend:1.1
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Issues and Solutions
-
Problem: Frontend can’t connect to backend API Causes: Network configuration, CORS issues, wrong API URL Solutions:
# Check container network docker network ls docker network inspect task-network # Verify containers are on same network docker inspect task-backend | grep NetworkMode docker inspect task-frontend | grep NetworkMode -
Problem: Database data disappears after container restart Cause: Not using volumes for data persistence Solution:
# Ensure volume is created and mounted docker volume ls docker inspect task-backend | grep -A 10 "Mounts" -
Problem: Port conflicts Cause: Ports already in use by other applications Solution:
# Check what's using the ports netstat -tulpn | grep :3000 netstat -tulpn | grep :5000 # Use different ports if needed docker run -p 3001:80 task-manager-frontend:1.0
Debugging Commands
# Container status and health
docker ps -a
docker stats
# Detailed container information
docker inspect <container-name>
# Container logs
docker logs <container-name>
docker logs --follow <container-name>
# Execute commands inside containers
docker exec -it task-backend bash
docker exec -it task-frontend sh
# Network troubleshooting
docker network ls
docker network inspect task-network
# Volume management
docker volume ls
docker volume inspect task-data
8. Best Practices and Professional Tips (5 minutes)
Development Workflow Best Practices
- Version Control: Keep Dockerfiles with your application code
- Environment Separation: Use different tags for dev/staging/production
- Resource Limits: Set memory and CPU limits for containers
- Security: Run containers as non-root users when possible
Production Considerations
- Health Checks: Implement proper health check endpoints
- Logging: Use structured logging and log aggregation
- Monitoring: Set up container monitoring and alerting
- Backup: Implement regular database backups
Next Steps in Your Learning Journey
- Week 2: Learn about volumes and data persistence in depth
- Advanced: Explore Docker Compose for easier multi-container management
- Production: Study container orchestration with Kubernetes
9. Summary and Review (5 minutes)
Key Takeaways
- Integration Success: Successfully combined all Docker concepts into a working application
- Real-World Application: Built something you could actually use and deploy
- Problem-Solving Skills: Learned to debug and troubleshoot containerized applications
Project Accomplishments
✅ Built a complete web application from scratch
✅ Containerized both frontend and backend components
✅ Implemented persistent data storage
✅ Created container networking for service communication
✅ Added proper error handling and user feedback
✅ Deployed and tested the complete application stack
Quick Review Questions
- Why did we create a custom Docker network for our containers?
- What would happen to our task data if we didn’t use a volume?
- How would you deploy this application to a production server?
Answers:
- To enable secure communication between containers and isolate them from other Docker networks
- Data would be lost every time the backend container is restarted or updated
- Use Docker Compose or container orchestration platforms, with proper security, monitoring, and backup strategies
For Instructors
Teaching Tips
- Encourage students to break things intentionally to learn debugging
- Show the application running in a browser during demonstrations
- Emphasize the real-world applicability of what they’ve built
- Help students connect today’s work to previous lessons
Assessment Ideas
- Code review: Students explain their Dockerfile choices and application architecture
- Troubleshooting exercise: Present broken configurations for students to fix
- Extension project: Students add new features to the application
Adaptation Notes
- For beginners: Focus on getting the basic application running successfully
- For advanced students: Add features like user authentication, data validation, or API documentation
- Online delivery: Use screen sharing to show the complete application workflow
- Time management: Core application should be working by 60 minutes, remaining time for enhancements
Celebration! 🎉
Congratulations! You’ve successfully built and deployed your first complete containerized application. You’ve demonstrated mastery of:
- Container lifecycle management
- Image building and tagging
- Multi-container networking
- Data persistence with volumes
- Real-world application architecture
- Debugging and troubleshooting
This is a significant milestone in your Docker journey. You now have the foundational skills to containerize any application and understand how Docker works in practice!