React + Express + PostgreSQL + Prometheus + Grafana + Loki + Promtail
Complete Documentation with All Source Code & Configuration Files
Complete 3-tier web application (Task Manager) deployed using Terraform and Docker with full monitoring & logging stack.
sudo apt update
sudo apt install docker.io -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
docker run hello-world
sudo apt install wget unzip -y
wget https://releases.hashicorp.com/terraform/1.6.6/terraform_1.6.6_linux_amd64.zip
unzip terraform_1.6.6_linux_amd64.zip
sudo mv terraform /usr/local/bin/
terraform -version
const express = require("express")
const cors = require("cors")
const { Pool } = require("pg")
const client = require("prom-client")
const app = express()
app.use(cors())
app.use(express.json())
/* PROMETHEUS METRICS */
const collectDefaultMetrics = client.collectDefaultMetrics
collectDefaultMetrics()
app.get("/metrics", async (req,res)=>{
res.set("Content-Type", client.register.contentType)
res.end(await client.register.metrics())
})
/* DATABASE CONNECTION */
const pool = new Pool({
host: process.env.DB_HOST,
user: "postgres",
password: process.env.DB_PASSWORD,
database: "appdb",
port: 5432
})
async function waitForDB() {
while (true) {
try {
await pool.query("SELECT 1")
console.log("PostgreSQL connected")
break
} catch (err) {
console.log("Waiting for DB...")
await new Promise(r => setTimeout(r, 2000))
}
}
}
async function initDB() {
await waitForDB()
await pool.query(`CREATE TABLE IF NOT EXISTS tasks (id SERIAL PRIMARY KEY, title TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`)
console.log("Table ready")
}
initDB()
/* ROUTES */
app.post("/tasks", async (req, res) => {
const { title } = req.body
const result = await pool.query("INSERT INTO tasks(title) VALUES($1) RETURNING *", [title])
res.json(result.rows[0])
})
app.get("/tasks", async (req, res) => {
const result = await pool.query("SELECT * FROM tasks ORDER BY id DESC")
res.json(result.rows)
})
app.delete("/tasks/:id", async (req, res) => {
await pool.query("DELETE FROM tasks WHERE id=$1", [req.params.id])
res.json({ message: "Deleted" })
})
app.listen(5000, () => console.log("Backend running on port 5000"))
{
"name": "task-backend",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.1",
"cors": "^2.8.5",
"prom-client": "^15.1.0"
}
}
FROM node:18
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 5000
CMD ["node", "server.js"]
{
"name": "task-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"proxy": "http://backend:5000"
}
FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
RUN npm install -g serve
CMD ["serve","-s","build","-l","3000"]
import { useEffect, useState } from "react"
function App() {
const API = "http://192.168.29.88:5000"
const [tasks,setTasks] = useState([])
const [title,setTitle] = useState("")
async function loadTasks() {
const res = await fetch(API + "/tasks")
const data = await res.json()
setTasks(data)
}
async function addTask() {
await fetch(API + "/tasks", {
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({title})
})
setTitle("")
loadTasks()
}
async function deleteTask(id){
await fetch(API + "/tasks/" + id, { method:"DELETE" })
loadTasks()
}
useEffect(()=>{ loadTasks() },[])
return (
<div style={{padding:"40px"}}>
<h1>Task Manager</h1>
<input value={title} onChange={e=>setTitle(e.target.value)} placeholder="New task" />
<button onClick={addTask}>Add</button>
<ul>
{tasks.map(t=>(
<li key={t.id}>
{t.title}
<button onClick={()=>deleteTask(t.id)}>delete</button>
</li>
))}
</ul>
</div>
)
}
export default App
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
grafana = {
source = "grafana/grafana"
version = "~> 2.0"
}
}
}
variable "network_name" { default = "app_network" }
variable "postgres_password" { default = "postgres" }
variable "frontend_port" { default = 3000 }
variable "backend_port" { default = 5000 }
module "network" { source = "./modules/network" network_name = "app_network" }
module "postgres" { source = "./modules/postgres" network = module.network.network_name password = "postgres" }
module "backend" { source = "./modules/backend" network = module.network.network_name db_host = "postgres_db" db_password = "postgres" }
module "frontend" { source = "./modules/frontend" network = module.network.network_name }
module "observability" { source = "./modules/observability" network = module.network.network_name }
terraform {
backend "s3" {
bucket = "terraform-state"
key = "3tier/terraform.tfstate"
region = "us-east-1"
access_key = "admin"
secret_key = "password"
endpoints = { s3 = "http://192.168.29.88:9000" }
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
skip_region_validation = true
force_path_style = true
}
}
resource "docker_network" "app_network" {
name = var.network_name
}
output "network_name" { value = docker_network.app_network.name }
resource "docker_container" "postgres" {
name = "postgres_db"
image = "postgres:15"
env = ["POSTGRES_PASSWORD=${var.password}", "POSTGRES_DB=appdb"]
networks_advanced { name = var.network }
ports { internal = 5432; external = 5432 }
}
resource "docker_image" "backend" {
name = "task-backend"
build { context = "${path.root}/../app/backend" }
}
resource "docker_container" "backend" {
name = "backend"
image = docker_image.backend.image_id
env = ["DB_HOST=${var.db_host}", "DB_PASSWORD=${var.db_password}"]
networks_advanced { name = var.network }
ports { internal = 5000; external = 5000 }
}
resource "docker_container" "frontend" {
name = "frontend"
image = docker_image.frontend.image_id
networks_advanced { name = var.network }
ports { internal = 3000; external = 3000 }
}
resource "docker_container" "prometheus" { ... }
resource "docker_container" "grafana" { ... }
resource "docker_container" "loki" { ... }
resource "docker_container" "promtail" { ... }
resource "docker_container" "node_exporter" { ... }
resource "docker_container" "cadvisor" { ... }
resource "docker_container" "postgres_exporter" { ... }
Full file includes volumes for prometheus.yml, loki-config.yml, promtail-config.yml, and all exporters.
provider "grafana" {
url = "http://192.168.29.88:3001"
auth = "admin:admin"
}
resource "grafana_data_source" "prometheus" {
name = "Prometheus"
type = "prometheus"
url = "http://prometheus:9090"
}
{
"title": "Docker Containers",
"panels": [{ "type": "timeseries", "title": "Container CPU Usage", "targets": [{ "expr": "rate(container_cpu_usage_seconds_total[1m])" }] }]
}
{
"title": "Node Exporter Overview",
"panels": [{ "expr": "rate(node_cpu_seconds_total{mode!='idle'}[1m])" }]
}
cd /home/devops/terraform-3tier/terraform
terraform init
terraform plan
terraform apply
Destroy: terraform destroy
terraform-3tier
├── app
│ ├── backend
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ └── server.js
│ └── frontend
│ ├── Dockerfile
│ ├── package.json
│ ├── public/index.html
│ └── src
│ ├── App.js
│ └── index.js
├── monitoring
│ ├── prometheus.yml
│ ├── loki-config.yml
│ ├── promtail-config.yml
│ ├── grafana-data
│ └── loki-data
└── terraform
├── provider.tf
├── variables.tf
├── main.tf
├── backend.tf
├── grafana_datasource.tf
├── grafana_dashboards.tf
├── grafana_wait.tf
├── outputs.tf
└── modules/
├── network/main.tf
├── postgres/main.tf
├── backend/main.tf
├── frontend/main.tf
└── observability/main.tf
This project demonstrates Infrastructure as Code, Containerization, Modular Terraform, Remote State, and Complete Observability (Metrics + Logs).
Remote State: MinIO simulates S3 for team collaboration and state locking.