Docker Mini Projects - Applying What We Learned

Pratik BapatPratik Bapat
3 min read

πŸ“Œ 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 file

  • Push 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:

☁️ 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

Pratik Bapat
Pratik Bapat