The Nginx Reverse Proxy
This site sits behind a reverse proxy that terminates TLS and routes requests to the right backend.

Photo · Wikimedia Commons
Where Cloud Meets This Server
Virtualization
This site runs on a small cloud VM, sharing physical hardware with other tenants via a hypervisor.
Reverse Proxies
Nginx terminates TLS and routes requests to the right backend — static files, PHP-FPM, or a local Flask app.
Backup & Storage
Private, mesh-VPN-secured cloud backup is one of the services offered directly from this infrastructure.
Hybrid Virtualization Platform
Two hypervisor platforms, one operational model. A Core site runs Proxmox VE; a Remote site runs VMware ESXi — and both report into the exact same monitoring stack, config-management story, and backup cadence.

Photo · Wikimedia Commons
Why two hypervisor platforms instead of forcing everything onto one: each site runs whatever actually fits its hardware and history, rather than a costly forced migration for the sake of uniformity. What stays uniform instead is the operational layer — one Ansible baseline applied fleet-wide, one Netdata/Prometheus/Loki/Grafana monitoring stack pulling from both sites over the mesh, and one backup cadence, whether the workload underneath is a Proxmox VM, an LXC container, or a VMware guest.
That combined footprint is 20+ hosts under active management today — the ~14-node home lab fleet plus the ~6-node cloud fleet — every node tracked against an 80% disk-capacity ceiling with automated cleanup, not a manual check someone remembers to run. Full detail on each side of this in the Home Lab & Self-Hosted Infrastructure case study and the Cloud Fleet & Infrastructure-as-Code case study.
Fleet Patterns That Actually Get Used
Managing a handful of VMs by hand works for a while. Managing more than three or four means these four things stop being optional: containerized services, config-as-code, centralized monitoring, and automated backups that leave the box they were taken on.
A typical three-service stack: app, database, and a proxy in front of both — the shape most small production apps actually run in.
version: "3.9"
services:
app:
build: .
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://app:${DB_PASSWORD}@db:5432/app
depends_on:
- db
networks:
- internal
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=app
volumes:
- db_data:/var/lib/postgresql/data
networks:
- internal
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
depends_on:
- app
networks:
- internal
networks:
internal:
volumes:
db_data:
caddy_data:
The same baseline applied to every node in a fleet — security posture, monitoring agent, and log shipping installed identically everywhere, instead of living in someone's memory of what they typed six months ago.
# playbooks/baseline.yml — applied to every node in the fleet
- hosts: all
become: true
vars:
admin_ssh_key: "{{ lookup('file', 'files/admin.pub') }}"
roles:
- role: common # timezone, unattended-upgrades, base packages
- role: security # ufw, fail2ban, sshd hardening
- role: monitoring # netdata agent + Prometheus node_exporter
- role: log_shipping # promtail -> centralized Loki
tasks:
- name: Ensure admin key is present
ansible.posix.authorized_key:
user: "{{ ansible_user }}"
key: "{{ admin_ssh_key }}"
state: present
- name: Enable and start fail2ban
ansible.builtin.systemd:
name: fail2ban
enabled: true
state: started
ansible-playbook -i inventory/hosts.yml playbooks/baseline.yml --limit fleet
One Prometheus instance pulling metrics from every node over the mesh VPN, feeding Grafana dashboards and alerting rules from a single pane of glass.
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'node'
static_configs:
- targets:
- 'node1.mesh.internal:9100'
- 'node2.mesh.internal:9100'
- 'node3.mesh.internal:9100'
labels:
fleet: 'primary'
- job_name: 'nginx'
static_configs:
- targets: ['node1.mesh.internal:9113']
rule_files:
- 'alerts/*.yml'
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
Dumps, verifies its own output isn't corrupt, ships the copy off-box, then prunes anything past the retention window. A backup that never leaves the machine it was taken on isn't a backup.
#!/usr/bin/env bash
# /usr/local/sbin/pg-backup.sh — nightly logical backup with retention
set -euo pipefail
DB_NAME="app"
BACKUP_DIR="/var/backups/postgres"
RETENTION_DAYS=14
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DEST="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
mkdir -p "$BACKUP_DIR"
pg_dump -U app -h 127.0.0.1 "$DB_NAME" | gzip > "$DEST"
# Verify the dump isn't empty/corrupt before trusting it
gzip -t "$DEST"
# Copy off-box — a backup on the same disk as the database it backs up
# is a second copy of the same failure, not a backup
rsync -az "$DEST" backup-host:/srv/backups/postgres/
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime "+${RETENTION_DAYS}" -delete
echo "$(date -Is) backup complete: $DEST"
Where This Runs For Real
This exact combination — Ansible-managed baseline, mesh VPN, centralized Netdata/Prometheus/Loki/Grafana monitoring, and fail2ban applied fleet-wide — runs a real multi-node cloud fleet today, including the incident where two nodes disappeared without warning and recovery happened from infrastructure-as-code instead of a scramble. Full details in the Cloud Fleet & Infrastructure-as-Code case study, and the home-lab side of the same architecture is in Home Lab Infrastructure.