This project is a fully containerized application stack built with production practices in mind. It runs a Flask web application backed by MySQL, served through NGINX as the sole public entry point. On top of that, a complete observability layer is wired in β Prometheus collects metrics from every service, Loki aggregates all container logs, and Grafana provides 7 pre-built dashboards covering the full stack.
Hadolint is used to lint app/Dockerfile against Dockerfile best-practice rules. Configuration lives in .hadolint.yaml at the project root.
Docker (no install needed):
docker run --rm -i hadolint/hadolint < app/DockerfileWith the project config applied:
docker run --rm -i \
-v "$(pwd)/.hadolint.yaml:/.config/hadolint.yaml" \
hadolint/hadolint < app/DockerfileIf hadolint is installed locally:
hadolint --config .hadolint.yaml app/DockerfileDL3008 β Pin versions in apt-get install β is intentionally ignored.
- Docker 20.10+ and Docker Compose v2
- Secrets files must exist before starting:
echo -n "root" > secrets/db_root_pw.txt
echo -n "admin" > secrets/db_admin_pw.txt./scripts/start.shAll 10 services start in dependency order. MySQL initializes the schema on first boot (db/init/01_schema.sql). Flask waits for MySQL to be healthy before starting. NGINX waits for Flask.
./scripts/stop.shContainers are removed, all named volumes (data) are preserved. To also wipe data:
docker compose down -vdocker compose build flask-app
docker compose up -d flask-appdocker compose logs -f # all services
docker compose logs -f flask-app mysql # specific servicesdocker compose ps| Interface | URL | Credentials |
|---|---|---|
| Application | http://localhost | β |
| Grafana | http://localhost:3000 | admin / admin |
| Prometheus | http://localhost:9090 | β |
| Alloy UI | http://localhost:12345 | β |
| cAdvisor | http://localhost:8080 | β |
| mysqld-exporter | http://localhost:9104/metrics | β |
| Blackbox Exporter | http://localhost:9115 | β |
Prometheus scrapes 6 targets every 15 seconds with 15-day retention:
| Job | Source | What it measures |
|---|---|---|
flask-app |
Flask /metrics |
Request rate, latency (p50/p95/p99), error rate per endpoint |
cadvisor |
cAdvisor | Per-container CPU, memory, network, filesystem |
mysqld-exporter |
mysqld-exporter | Query throughput, connections, InnoDB buffer pool, slow queries |
blackbox-http |
Blackbox β NGINX | HTTP uptime, status code, response time phases |
blackbox-exporter |
Blackbox self | Exporter health |
prometheus |
Prometheus self | Scrape health, TSDB stats |
Alloy also collects host-level metrics (CPU, memory, disk, network) via its built-in node exporter and pushes them to Prometheus via remote_write.
Alloy tails all container logs via the Docker socket and ships them to Loki with container and service labels. MySQL slow query log gets special treatment β multiline entries are reassembled and the query_time label is extracted for filtering.
7 pre-provisioned Grafana dashboards, auto-loaded on startup:
| Dashboard | Data Source | What's inside |
|---|---|---|
| Stack Overview | Prometheus + Loki | Single-pane-of-glass β all service health, links to every other dashboard |
| Flask Application | Prometheus | Request rate, error rate, p50/p95/p99 latency, per-endpoint breakdown |
| Docker Containers | Prometheus | Per-container CPU, memory, network β links to logs |
| MySQL | Prometheus + Loki | Query throughput by type, connections, InnoDB, slow query log panel |
| Container Logs | Loki | Live log viewer, log volume, MySQL slow log, journal errors |
| Node Exporter Full | Prometheus | Full host system metrics |
| Prometheus Blackbox Exporter | Prometheus | HTTP probe status, response time breakdown by phase |
Dashboards are cross-linked β container metrics panels link directly to Container Logs filtered to that container.
Database credentials are managed via Docker Secrets β never plain environment variables.
secrets/
βββ db_root_pw.txt # MySQL root password
βββ db_admin_pw.txt # MySQL app user password (used by Flask + mysqld-exporter)
See secrets/README.md.
docker-stack/
βββ docker-compose.yml
βββ .hadolint.yaml # Hadolint Dockerfile linting config
βββ app/ # Flask application
β βββ Dockerfile
β βββ app.py
β βββ requirements.txt
β βββ templates/index.html
βββ configs/ # All service configs (mounted read-only)
β βββ nginx/default.conf
β βββ prometheus/prometheus.yml
β βββ loki/loki.yml
β βββ alloy/config.alloy
β βββ blackbox/blackbox.yml
β βββ grafana/provisioning/
β βββ datasources/ # Prometheus + Loki auto-wired
β βββ dashboards/json/ # 7 pre-built dashboards
βββ db/init/01_schema.sql # MySQL schema, auto-run on first start
βββ secrets/ # Never committed β gitignored
β βββ db_root_pw.txt
β βββ db_admin_pw.txt
βββ scripts/
βββ start.sh # docker compose up -d
βββ stop.sh # docker compose down (volumes preserved)
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |














