Containerizing a Full-Stack Application with Advanced Monitoring
Table of Contents
A. Introduction
B. Prerequisites
C. Project Setup
D. Backend Configuration
E. Frontend Configuration
F. Docker Setup
G. Docker Compose Configuration
H. Reverse Proxy & SSL Setup
I. Database Management
F. Setting up the monitoring stack
Introduction
In this project, we will containerize and deploy a multi-tier web application using Docker, Docker Compose, and reverse proxy configurations. The application consists of a React frontend that interacts with a FastAPI backend, which is backed by a PostgreSQL database. To ensure smooth monitoring and management of the application's performance, we will integrate a Monitoring Stack that includes Prometheus for metrics collection, Grafana for data visualization, Loki for log aggregation, and cAdvisor for container performance monitoring.
The overall architecture will use Docker Compose for orchestration, ensuring that all services are interconnected and can communicate seamlessly. The services will be routed through a reverse proxy, either Traefik or Nginx Proxy Manager, to ensure proper URL routing, security, and access control for both the application stack and the monitoring stack.
Prerequisites
- Linux-based operating system (Ubuntu, CentOS, etc.)
- Docker and Docker Compose installed
- Cloud provider account (AWS, Google Cloud, or Azure) for deployment
- Domain name
- Basic knowledge of:
- Docker & Docker Compose
- React & FastAPI
- Prometheus, Grafana, Loki, and cAdvisor
Project Setup
A. Clone the repository:
git clone https://github.com/The-DevOps-Dojo/cv-challenge01.git
cd cv-challenge01
Backend Configuration
A. Navigate to the backend directory:
cd backend
B. Install Poetry (dependency manager):
-
For macOS/Linux:
curl -sSL https://install.python-poetry.org | python3 -
For Windows: Follow Poetry's documentation
C. Verify Poetry installation:
poetry --version
D. Install project dependencies:
poetry install
PostgreSQL Setup
A. Install PostgreSQL:
sudo apt update
sudo apt install postgresql postgresql-contrib
B. Switch to PostgreSQL user:
sudo -i -u postgres
psql
C. Create database and user:
CREATE USER app WITH PASSWORD 'changethis123';
CREATE DATABASE app;
\c app
GRANT ALL PRIVILEGES ON DATABASE app TO app;
GRANT ALL PRIVILEGES ON SCHEMA public TO app;
\q
exit
D. Configure environment variables:
Create/edit .env
file in the backend directory:
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=app
POSTGRES_USER=app
POSTGRES_PASSWORD=changethis123
E. Initialize database:
poetry run bash ./prestart.sh
F. Start backend service:
poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
Frontend Configuration
A. In a new terminal, navigate to frontend:
cd frontend
B. Install Node.js and dependencies:
sudo apt update
sudo apt install nodejs npm
npm install
C. Start frontend server:
npm run dev -- --host
When attempting to log in, you might encounter a network error:
Frontend Configuration Updates
A. Update API URL in frontend .env
:
VITE_API_URL=http://<your_server_IP>:8000
After updating the frontend configuration, you may encounter a CORS error:
B. Update CORS settings in backend .env
:
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://<your_server_IP>:5173"
C. Use these default login credentials:
- Username:
chanllenge@devopsdojo.com
- Password:
devopsdojo57
After successful configuration, you should see:
Accessing Swagger Documentation
Access the Swagger API documentation at http://<your_server_IP>:8000/docs
:
Docker Setup
Backend Dockerfile
Create a Dockerfile
in the backend directory:
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10
WORKDIR /app/
RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \
cd /usr/local/bin && \
ln -s /opt/poetry/bin/poetry && \
poetry config virtualenvs.create false
COPY ./pyproject.toml ./poetry.lock* /app/
RUN poetry install --no-root --only main
ENV PYTHONPATH=/app
COPY ./alembic.ini /app/
COPY ./prestart.sh /app/
COPY ./app /app/app
# Ensure prestart.sh is executable
RUN chmod +x /app/prestart.sh
# Override the CMD to run the prestart.sh script and then start Uvicorn
CMD ["/bin/bash", "-c", "/app/prestart.sh && uvicorn app.main:app --host 0.0.0.0 --port 8000"]
Frontend Dockerfile
Create a Dockerfile
in the frontend directory:
# Use the latest official Node.js image as a base
FROM node:latest
# Set the working directory
WORKDIR /app
# Copy the application files
COPY . .
# Install dependencies
RUN npm install
# Expose the port the development server runs on
EXPOSE 5173
# Run the development server
CMD ["npm", "run", "dev", "--", "--host"]
Docker Compose Configuration
A. Create a docker-compose.yml
file in the project root:
services:
backend:
build:
context: ./backend
container_name: backend_fastapi_app
ports:
- "8000:8000"
depends_on:
- db
env_file:
- ./backend/.env
frontend:
build:
context: ./frontend
container_name: frontend_nodejs_app
ports:
- "5173:5173"
env_file:
- ./frontend/.env
db:
image: postgres:latest
container_name: postgres_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- ./backend/.env
adminer:
image: adminer
container_name: adminer-service
ports:
- "8080:8080"
proxy:
image: jc21/nginx-proxy-manager:latest
container_name: nginx_proxy_manager
ports:
- "80:80"
- "443:443"
- "81:81"
environment:
DB_SQLITE_FILE: "/data/database.sqlite"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
- backend
- frontend
- adminer
volumes:
postgres_data:
data:
letsencrypt:
B. Start the services:
# Build and start all services
docker compose up --build -d
# Check service status
docker compose ps
# View logs
docker compose logs -f
Reverse Proxy and SSL Setup
A. Access Nginx Proxy Manager
- URL:
http://<your-server-ip>:81
- Default credentials:
-
Email:
admin@example.com
-
Password:
changeme
-
Email:
B. Configure Proxy Hosts for Services
-
Add Frontend Proxy Host
-
Domain:
your_domain.com
-
Forward Hostname:
frontend
-
Forward Port:
5173
-
Options:
- Enable Block Common Exploits
-
Domain:
-
SSL:
- Request a Let's Encrypt certificate.
- Enable Force SSL.
C. Configure Advanced Proxy Settings
Go to the Advanced tab for the backend service and add the following routing rules:
location /api {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /docs {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
-
Add Nginx Manager Proxy Host (API)
-
Domain:
proxy.your_domain.com
-
Forward Hostname:
proxy
-
Forward Port:
81
- SSL: Follow the same steps as above.
-
Domain:
-
Add Adminer Proxy Host
-
Domain:
db.your_domain.com
-
Forward Hostname:
db
-
Forward Port:
8080
- SSL: Follow the same steps as above.
-
Domain:
-
Redirect
www
to Non-www
:- Navigate to Proxy Hosts in Nginx Proxy Manager.
- Add a new Proxy Host for
www.your_domain.com
. -
Forward Hostname:
your_domain.com
-
Forward Port:
443
- Enable Force SSL.
D. Verify Service Accessibility
After completing the above configurations:
-
Frontend:
https://your_domain.com
-
Backend API:
https://api.your_domain.com
-
Adminer:
https://db.your_domain.com
Database Management
A. Access Adminer
- Navigate to
https://db.your_domain.com
. - Login with the credentials from the backend's
.env
file:- System: PostgreSQL
-
Server:
db
B. Manage Database
- Use Adminer to perform tasks like creating tables, managing records, or running SQL queries.
-
Example Usage:
- List All Tables: Access the left navigation menu in Adminer after logging in.
- Run Queries: Use the SQL tab to execute custom queries.
By following these steps, you have a structured reverse proxy and database management setup. The dashboard ensures www
requests are redirected seamlessly, and all services are accessible via specific subdomains.
Setting Up a Complete Monitoring Stack for Your Containerized Application
NOTE: Please update the application compose file with a shared network. This is to make all services communicate internally with each other as shown below
services:
backend:
build:
context: ./backend
container_name: backend_fastapi_app
ports:
- "8000:8000"
depends_on:
- db
env_file:
- ./backend/.env
networks:
- shared-network
frontend:
build:
context: ./frontend
container_name: frontend_nodejs_app
ports:
- "5173:5173"
env_file:
- ./frontend/.env
networks:
- shared-network
db:
image: postgres:latest
container_name: postgres_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- ./backend/.env
networks:
- shared-network
adminer:
image: adminer
container_name: adminer-service
ports:
- "8080:8080"
networks:
- shared-network
proxy:
image: jc21/nginx-proxy-manager:latest
container_name: nginx_proxy_manager
ports:
- "80:80"
- "443:443"
- "81:81"
environment:
DB_SQLITE_FILE: "/data/database.sqlite"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
- backend
- frontend
- adminer
networks:
- shared-network
volumes:
postgres_data:
data:
letsencrypt:
networks:
shared-network:
name: shared-network
Now that we have our application running, let's add comprehensive monitoring capabilities. We'll set up Prometheus for metrics collection, Grafana for visualization, cAdvisor for container metrics, and Loki with Promtail for log aggregation.
Getting Started
First, create a new file called monitoring-compose.yml
:
services:
grafana:
image: grafana/grafana
container_name: grafana
restart: unless-stopped
environment:
- GF_SERVER_DOMAIN=your.domain
- GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s/grafana
- GF_SERVER_SERVE_FROM_SUB_PATH=true
- GF_AUTH_PROXY_ENABLED=false
- GF_SECURITY_ALLOW_EMBEDDING=true
- GF_SECURITY_COOKIE_SAMESITE=none
- GF_SECURITY_COOKIE_SECURE=true
- GF_SESSION_PROVIDER=memory
- GF_SESSION_PROVIDER_CONFIG=sessions
ports:
- "3000:3000"
volumes:
- grafana-storage:/var/lib/grafana
networks:
- shared-network
prometheus:
image: prom/prometheus
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--web.external-url=/prometheus'
- '--web.route-prefix=/prometheus'
ports:
- "9090:9090"
restart: unless-stopped
volumes:
- ./prometheus:/etc/prometheus
- prom_data:/prometheus
depends_on:
- cadvisor
networks:
- shared-network
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
ports:
- "8083:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
networks:
- shared-network
loki:
image: grafana/loki:3.0.0
container_name: loki
user: "10001"
volumes:
- ./loki-config.yaml:/mnt/config/loki-config.yaml
- ./data:/tmp/loki
ports:
- "3100:3100"
command: -config.file=/mnt/config/loki-config.yaml
networks:
- shared-network
promtail:
image: grafana/promtail:3.0.0
container_name: promtail
depends_on:
- loki
volumes:
- ./promtail-config.yaml:/mnt/config/promtail-config.yaml
- /var/log:/var/log
command: -config.file=/mnt/config/promtail-config.yaml
networks:
- shared-network
volumes:
grafana-storage:
prom_data:
networks:
shared-network:
external: true
Important Configuration Notes
Network Configuration
Remember the shared-network
we created earlier? Our monitoring stack will use the same network to communicate with our application. If you haven't created it yet:
docker network create shared-network
This network allows our monitoring services to communicate with both the application services and each other.
Grafana Configuration
Looking at the Grafana service, you'll notice several environment variables:
environment:
- GF_SERVER_DOMAIN=your.domain
- GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s/grafana
- GF_SERVER_SERVE_FROM_SUB_PATH=true
Replace your.domain
with your actual domain name. These settings ensure Grafana works correctly under a subpath (/grafana) with your domain.
Service Configuration Files
Before starting the stack, you'll need configuration files for Prometheus, Loki, and Promtail:
- Create a
prometheus.yml
file in a newprometheus
directory:
global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: []
scheme: http
timeout: 10s
api_version: v2
scrape_configs:
- job_name: prometheus
honor_timestamps: true
scrape_interval: 15s
scrape_timeout: 10s
metrics_path: /prometheus/metrics
scheme: http
static_configs:
- targets: ['prometheus:9090']
- job_name: cadvisor
scrape_interval: 5s
metrics_path: /metrics
scheme: http
static_configs:
- targets: ['cadvisor:8080']
- Download Loki and Promtail configurations:
# Get Loki config
wget https://raw.githubusercontent.com/grafana/loki/v3.0.0/cmd/loki/loki-local-config.yaml -O loki-config.yaml
# Get Promtail config
wget https://raw.githubusercontent.com/grafana/loki/v3.0.0/clients/cmd/promtail/promtail-docker-config.yaml -O promtail-config.yaml
Nginx Proxy Manager Configuration
Add these locations to your existing proxy host configuration:
location /prometheus {
proxy_pass http://prometheus:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /grafana {
proxy_pass http://grafana:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Starting the Monitoring Stack
- Create required directories and set permissions:
# Create directories
mkdir -p prometheus data
# Set Loki permissions (important!)
sudo chown -R 10001:10001 ./data
sudo chmod -R 755 ./data
- Start the stack:
docker compose -f monitoring-compose.yml up -d
- Verify services are running:
docker compose -f monitoring-compose.yml ps
Accessing Your Monitoring Stack
Once everything is running, you can access:
- Grafana:
https://your.domain/grafana
- Prometheus:
https://your.domain/prometheus
Troubleshooting Tips
If you encounter issues:
- Check service logs:
docker compose -f monitoring-compose.yml logs grafana
docker compose -f monitoring-compose.yml logs prometheus
- Verify network connectivity:
docker network inspect shared-network
Setting Up Your Monitoring Dashboard
Now that our monitoring stack is running, let's configure our dashboards and data sources for effective monitoring.
Adding Data Sources to Grafana
A. Access Grafana at https://your.domain/grafana
- Default credentials: admin/admin
- You'll be prompted to change the password on first login
B. Configure Prometheus Data Source:
- Go to Settings (⚙️) > Data Sources
- Click "Add data source"
- Select "Prometheus"
- Configure:
- Name: Prometheus
- URL:
http://prometheus:9090/prometheus
#because of the sub-path requirements in our projects - Access: Server (default)
- Click "Save & Test"
Setting Up Loki for Log Aggregation
A. Add Loki Data Source:
- Go to Settings (⚙️) > Data Sources
- Click "Add data source"
- Select "Loki"
- Configure:
- Name: Loki
- URL:
http://loki:3100
- Access: Server (default)
- Click "Save & Test"
References
"My HNG Journey: Stage Two Containerization and Deployment of a Three-Tier Application Using Docker and Nginx Proxy Manager"
A detailed guide on deploying a three-tier application with Docker and Nginx Proxy Manager, which influenced the reverse proxy configuration steps.
Read more on dev.toTechdox Documentation
Comprehensive documentation for setting up each monitoring services.
Visit Docs Here-
Youtube videos
Insightful YouTube videos with insight on deployment strategies and troubleshooting shared, particularly for Loki and Nginx.
Top comments (0)