Skip to content

est-harsh/docker-3tier-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Docker 3-Tier Application — FiftyFive Technologies DevOps Intern Assignment

A fully containerised 3-tier web application built with Nginx · Python/Flask · MySQL 8.0.


Architecture

  Browser
    │
    │  HTTP :80
    ▼
┌─────────────────────────────┐
│  frontend  (nginx:alpine)   │  port 80 (host) → 8080 (container)
│  • serves index.html        │
│  • /api/* → proxy backend   │
└────────────┬────────────────┘
             │  HTTP :5000  (Docker network: app_network)
             ▼
┌─────────────────────────────┐
│  backend  (python:alpine)   │  port 5000
│  • GET /      → 200 JSON    │
│  • GET /health → DB status  │
└────────────┬────────────────┘
             │  TCP :3306   (Docker network: app_network)
             ▼
┌─────────────────────────────┐
│  db  (mysql:8.0)            │  port 3306 (internal only)
│  • named volume: mysql_data │
└─────────────────────────────┘

All three containers share the custom bridge network app_network and communicate using Docker service names as hostnames (no hardcoded IPs).


Setup Instructions

Prerequisites

  • Docker Engine ≥ 24 and Docker Compose v2

1 — Clone & configure

git clone <your-repo-url>
cd docker-3tier-app
cp .env.example .env       # fill in your passwords

2 — Build & start (single command)

docker compose up --build

Compose starts the services in dependency order: db → (healthy) → backend → (healthy) → frontend

Open http://localhost in your browser.


Explanation

How the backend waits for MySQL

Two complementary mechanisms are in place so the backend never crashes permanently:

  1. depends_on: condition: service_healthy in docker-compose.yml — Compose will not start the backend container at all until the db healthcheck (mysqladmin ping) passes. depends_on alone is not used.

  2. wait_for_db() retry loop in app.py — when the Flask process starts it attempts a real MySQL connection up to 40 times (3 s apart). If it still can't connect it logs a warning and starts the HTTP server anyway, reporting unhealthy on /health until the database becomes reachable again. The process never exits, so Docker never has to restart it due to a DB blip.

How Nginx gets the backend URL

BACKEND_URL (e.g. backend:5000) is defined in .env. Compose injects it as an environment variable into the frontend container. The container's start command runs:

envsubst '${BACKEND_URL}' \
  < /etc/nginx/templates/nginx.conf.template \
  > /etc/nginx/conf.d/default.conf

envsubst only replaces ${BACKEND_URL}; all other nginx variables ($host, $remote_addr, etc.) are left intact. No URL is hardcoded.

How services communicate

All containers are on the app_network bridge network. Docker's embedded DNS resolves service names to container IPs automatically:

From To Address used
browser nginx localhost:80
nginx backend backend:5000
backend db db:3306

Testing Steps

Access the frontend

open http://localhost          # macOS
xdg-open http://localhost      # Linux

The page displays live JSON responses from both API endpoints.

Hit the API directly through the Nginx proxy

# Basic OK response
curl http://localhost/api/

# Database health check
curl http://localhost/api/health

View all logs

docker compose logs -f
docker compose logs -f backend   # backend only

Health-check status

docker compose ps    # shows health: healthy / starting / unhealthy

Failure Scenario — MySQL Restart

What happens

docker restart docker-3tier-app-db-1
Time What happens
0 s MySQL container stops.
0–30 s GET /api/health returns 500 unhealthy — backend is running but DB is unreachable.
~30–60 s MySQL restarts and initialises.
~60 s MySQL healthcheck passes again.
Next request Backend reconnects transparently — wait_for_db is not re-run, but get_db_connection() creates a fresh connection on every /health call, so recovery is automatic.

Recovery time

Typically 30–60 seconds (MySQL initialisation time).

How it was handled

  • The backend never exits when the DB goes away — the Flask process stays alive and continues serving GET / with 200 OK.
  • GET /health returns 500 while MySQL is down, giving an honest status signal.
  • Once MySQL is back, the very next call to get_db_connection() succeeds and /health returns 200 healthy again — no restart of the backend is needed.

Bonus Features

Feature Implementation
Multi-stage build backend/Dockerfile — Stage 1 installs deps into a venv; Stage 2 copies only the venv + source.
Non-root user Backend runs as appuser; Nginx runs as nginx (port 8080, no root required).

Repository Structure

.
├── frontend/
│   ├── Dockerfile              # nginx:alpine, non-root, envsubst
│   ├── nginx.conf              # main config — no 'user' directive, /tmp paths
│   ├── nginx.conf.template     # server block with ${BACKEND_URL} placeholder
│   ├── index.html              # static page with live API status cards
│   └── .dockerignore
├── backend/
│   ├── Dockerfile              # multi-stage python:alpine, non-root appuser
│   ├── app.py                  # Flask app — GET /, GET /health, wait_for_db
│   ├── requirements.txt
│   └── .dockerignore
├── docker-compose.yml
├── .env.example                # ← commit this
├── .env                        # ← DO NOT commit
└── README.md

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors