Docker Mini Projects - Applying What We Learned

3 min read
Table of contents
- π Overview
- ποΈ Project Structure
- π§ Step 1: Create the Flask Backend
- π³ Step 2: Create a Multi-Stage Alpine Dockerfile
- π§Ύ Step 3: Use .env File for Secrets
- π Step 4: Docker Compose for App + DB
- π Step 5: Prometheus Monitoring Setup
- π Step 6: Build and Deploy
- βοΈ Step 7: Push to Docker Hub
π Overview
In this hands-on DevOps mini project, we will:
Build a Flask-based Todo web app
Connect it to a PostgreSQL database
Use Alpine-based multi-stage Docker builds
Deploy using Docker Compose on a single node
Implement health checks
Expose Prometheus metrics for monitoring
Configure everything using a
.env
filePush your image to Docker Hub
Letβs dive in! π§βπ»
ποΈ Project Structure
todo-flask-app/
β
βββ .env # Environment variables (used in docker-compose)
βββ docker-compose.yml # Defines services: web + db
β
βββ db_init/ # β
Auto-run DB init scripts (PostgreSQL)
β βββ init.sql # Creates DB, tables, and inserts seed data
β
βββ backend/ # Flask application source code
βββ app.py # Main Flask app
βββ Dockerfile # Multi-stage Alpine build
βββ requirements.txt # Python dependencies
βββ templates/ # HTML templates (Jinja2)
βββ index.html
π§ Step 1: Create the Flask Backend
backend/app.py
from flask import Flask, render_template, request, redirect
import psycopg2
import os
import time
from prometheus_client import Counter, generate_latest, CONTENT_TYPE_LATEST
import logging
app = Flask(__name__)
todo_counter = Counter('todo_created_total', 'Total number of todos created')
logging.basicConfig(level=logging.INFO)
def get_db_connection():
conn = psycopg2.connect(
dbname=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
host=os.environ['DB_HOST']
)
return conn
@app.route('/')
def index():
conn = get_db_connection()
cur = conn.cursor()
cur.execute('SELECT * FROM tasks')
tasks = cur.fetchall()
conn.close()
return render_template('index.html', tasks=tasks)
@app.route('/add', methods=['POST'])
def add():
task = request.form['task']
conn = get_db_connection()
cur = conn.cursor()
cur.execute("INSERT INTO tasks (title) VALUES (%s)", (task,))
conn.commit()
conn.close()
todo_counter.inc()
return redirect('/')
@app.route('/health')
def health():
return "OK", 200
@app.route('/metrics')
def metrics():
return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
backend/templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>
<form action="/add" method="post">
<input type="text" name="task" placeholder="Add a task">
<button type="submit">Add</button>
</form>
<ul>
{% for task in tasks %}
<li>{{ task[1] }}</li>
{% endfor %}
</ul>
</body>
</html>
backend/requirements.txt
Flask==2.2.2
psycopg2-binary==2.9.3
prometheus_client==0.17.1
π³ Step 2: Create a Multi-Stage Alpine Dockerfile
backend/Dockerfile
FROM python:3.10-alpine as builder
WORKDIR /app
COPY requirements.txt .
RUN apk add --no-cache gcc musl-dev libffi-dev postgresql-dev && pip install --prefix=/install -r requirements.txt
FROM python:3.10-alpine
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
π§Ύ Step 3: Use .env File for Secrets
.env
POSTGRES_DB=todoapp
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_HOST=db
- Create an π init script
init.sql
:
CREATE DATABASE todoapp;
\c todoapp
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Optional seed data
INSERT INTO tasks (title, completed) VALUES
('Sample Task 1', FALSE),
('Sample Task 2', TRUE);
π Step 4: Docker Compose for App + DB
docker-compose.yml
version: '3.10'
services:
web:
build: ./backend
environment:
DB_HOST: ${POSTGRES_HOST}
DB_PORT: 5432
DB_NAME: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "5000:5000"
depends_on:
- db
healthcheck:
test: ["CMD", "curl", "-f", "http://192.168.0.150:5000/health"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "monitoring=true"
networks:
- todo-net
db:
image: postgres:14-alpine
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
- ./db_init:/docker-entrypoint-initdb.d
networks:
- todo-net
ports:
- "5432:5432"
volumes:
pgdata:
networks:
todo-net:
π Step 5: Prometheus Monitoring Setup
Note: Promethues already configured and running on port 9090 so only adding following lines to scrape metrics of flask app
Update /etc/prometheus/prometheus.yml
:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'flask-todo-app'
static_configs:
- targets: ['192.168.0.150:5000']
Then restart Prometheus:
sudo systemctl restart prometheus
π Step 6: Build and Deploy
docker compose up --build -d
docker ps
Access:
Add new todo
- http://192.168.0.150:5000/metrics (Metrics)
- http://192.168.0.150:8000 (Grafana)
βοΈ Step 7: Push to Docker Hub
docker tag todo-flask-app <your_dockerhub_username>/todo-flask:latest
docker push <your_dockerhub_username>/todo-flask:latest
0
Subscribe to my newsletter
Read articles from Pratik Bapat directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
