π Web Service Outage Troubleshooting Runbook
This runbook provides step-by-step instructions to diagnose and resolve web service outages in the Home Lab. Traffic reaches the stack via one of two entry paths β External through Cloudflare, or Internal through Pi-hole DNS β and both paths converge at rproxy-0. OAuth2 identity verification via Azure Entra ID is handled at rproxy-0 and may or may not be enabled depending on the service.
β‘ Important: Start with 0οΈβ£ Quick Triage. A stopped backend service or a powered-off VM is the most common cause of an outage, and it shows up as a 502 Bad Gateway from Cloudflare or nginx β which looks like a proxy or Cloudflare problem but isnβt.
πΊοΈ End-to-End Traffic Flow
EXTERNAL PATH INTERNAL PATH
[ Browser / Client ] [ Browser / Client ]
| |
| HTTPS (443) | HTTPS (443)
v v
[ Cloudflare ] [ Pi-hole DNS ]
WAF Β· DDoS Β· TLS Edge Β· CDN 192.168.20.253 (primary)
Anycast β Nearest PoP |
| |
| Origin Pull (Proxied HTTPS) | Resolved A record β rproxy-0
| |
+βββββββββββββββββββ+βββββββββββββββββββ+
|
v
[ rproxy-0 ]
nginx Β· upstream block
Static IP Β· entry proxy
|
βββββββββββββββββββββββ
(optional) OAuth2 Proxy
Azure Entra ID β identity
verification before upstream
βββββββββββββββββββββββ
|
βββββββββββ΄ββββββββββ
v v
[ rproxy-1 ] [ rproxy-2 ]
App Reverse Proxy App Reverse Proxy
βββββββββββ¬ββββββββββ
|
v
[ Backend Service ]
[ backend-01 :8080 ]
0οΈβ£ Quick Triage β Is the Backend VM and Service Up?
Do this first, regardless of entry path. If the backend is down, every proxy tier in front of it will return 502 Bad Gateway (or Cloudflare 502 / 521), and the Cloudflare, Pi-hole, and rproxy checks below will all look healthy.
Run from the Ansible control node, using the inventory the service is deployed from (example: Lidarr on lidarr-0).
Step 1 β Is the VM up?
ansible -i inventory/services/inventory.ini lidarr -m ping
- SUCCESS / pong β VM is up and reachable. Go to Step 2.
- UNREACHABLE β The VM is down, hung, or has lost networking. Check it in Proxmox:
ssh <pve-node>
sudo qm list | grep -i lidarr
# or across the whole cluster:
sudo pvesh get /cluster/resources --type vm --output-format text | grep -i lidarr
- stopped β Start it (
sudo qm start <vmid>), wait for it to boot, then repeat Step 1. - running but unreachable β Open the VM console in the Proxmox UI and check for a hung boot, a full disk, or a lost network config.
Step 2 β Is the service running on the VM?
Docker-based service:
ansible -i inventory/services/inventory.ini lidarr -b -a "docker ps -a"
- Container Exited or Restarting β Check logs with
docker logs --tail 100 <container>, then restart the container or re-run the serviceβs deploy playbook.
Native (systemd) service:
ansible -i inventory/services/inventory.ini lidarr -b -a "systemctl status <service> --no-pager"
Step 3 β Does the service answer locally?
ansible -i inventory/services/inventory.ini lidarr -a "curl -Is http://localhost:8686"
- Any HTTP response β The backend is serving. Continue with the entry-path sections below.
- Connection refused / timeout β The process is up but not listening. Check the application logs and resource usage (see 5οΈβ£).
β‘ Note: If the VM and service are both healthy, move on to 1οΈβ£ External or 2οΈβ£ Internal based on where the client is coming from.
1οΈβ£ External Path β Check Cloudflare
Follow this section if the client is coming from the internet and traffic enters via Cloudflare.
Confirm the site is reachable from the public internet:
curl -Isk https://your-domain.example.com | head -5
- HTTP 200 / 301 / 302 β Edge is responding. Skip to step 5.
- HTTP 502 β Usually the backend service or its VM is down. Re-check 0οΈβ£ Quick Triage, then rproxy-0 (3οΈβ£) and rproxy-1/2 (4οΈβ£).
- HTTP 521 / 522 / 523 / 524 β Cloudflare cannot reach the origin. Work through this section.
- Timeout or DNS NXDOMAIN β Likely a Cloudflare DNS misconfiguration.
Cloudflare Dashboard Checks
- Log in to dash.cloudflare.com and open your zone.
- Confirm the DNS A record for your domain is proxied (orange cloud icon).
- Check Analytics & Logs β Traffic for a spike in 5xx errors.
- Check Security β WAF for any rules that may be blocking origin traffic.
- Confirm SSL/TLS mode is set to Full (Strict).
- Visit cloudflarestatus.com to rule out a platform-wide incident.
β‘ Important: A grey-cloud (DNS-only) record means traffic bypasses Cloudflare entirely. Verify proxy status before anything else.
2οΈβ£ Internal Path β Check Pi-hole DNS
Follow this section if the client is on the internal network and traffic is routed via Pi-hole DNS resolution.
Verify that Pi-hole resolves the domain to the rproxy-0 address:
dig lidarr.refol.us @192.168.20.253
Expected: The A record should point to the rproxy-0 IP address.
Pi-hole Service Health
ssh <pi-hole-host>
sudo systemctl status pihole-FTL
sudo systemctl status lighttpd
pihole -t
Upstream Resolver Reachability
Perform the following from proxy-0.
curl http://192.168.20.211
curl http://192.168.20.212
- If 192.168.20.211 is unreachable β check router/firewall rules. Pi-hole will fall back to 192.168.20.212 automatically.
- If both are unreachable β check the upstream network segment and routing.
Flush Pi-hole DNS Cache
pihole restartdns
β‘ Important: Always verify both upstream IPs individually. A silent failure on .211 with no failover to .212 is a common cause of intermittent internal DNS outages.
3οΈβ£ Shared Path β rproxy-0 Entry Proxy
Both the External and Internal paths converge here. Follow this section regardless of entry path.
Check the nginx service status and validate the running configuration:
ssh rproxy-0
sudo systemctl status nginx
sudo nginx -t
Review recent errors:
sudo tail -50 /var/log/nginx/error.log
sudo tail -50 /var/log/nginx/access.log | grep " 5[0-9][0-9] "
Inspect the upstream block to confirm rproxy-1 and rproxy-2 are correctly defined:
grep -A 20 "upstream" /etc/nginx/nginx.conf
# or if configs are split:
grep -rA 20 "upstream" /etc/nginx/conf.d/
If a config change is needed, always test before reloading:
sudo nginx -t && sudo systemctl reload nginx
OAuth2 Proxy β Azure Entra ID (Optional)
β‘ Note: A 500 Internal Server Error shown before the OAuth2 sign-on screen in the browser typically indicates that the OAuth2 service for the application is down.
rproxy-0 may be configured to run an OAuth2 proxy in front of upstream services. This is not enabled for every service β confirm whether the affected service uses it before investigating.
If OAuth2 is enabled for the service, check the proxy process and logs:
sudo systemctl status oauth2-proxy_lidarr.refol.us.service
sudo journalctl -u oauth2-proxy_lidarr.refol.us.service -n 100 --no-pager
β‘ Note: each web service that is configured using its own oauth2 service.
β‘ Important: An expired client secret causes a hard auth failure with no useful browser-side error. Check secret expiry first when redirect loops are observed.
β‘ Note: A 500 Internal Server Error shown in the browser after successfully authenticating in Azure typically indicates that there is an issue with the Entra ID service for the application is down. Journalctl will also show the message βError redeeming code during OAuth2 callback: token exchange failedβ when tailing the particular service journal entries.
Confirm the Entra ID OIDC metadata endpoint is reachable from rproxy-0:
curl -s https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration \
| python3 -m json.tool | head -20
Check the App Registration in portal.azure.com β Microsoft Entra ID β Manage β App Registrations β All applications:
- Confirm Redirect URIs match your domain exactly β trailing slashes and casing matter.
- Check Certificates & Secrets for any expired client secrets.
- Navigate to Security β Conditional Access β Policies and look for any recently modified policy targeting the app or its users.
β‘ Important: An expired client secret causes a hard auth failure with no useful browser-side error. Check secret expiry first when redirect loops are observed.
4οΈβ£ Shared Path β rproxy-1 / rproxy-2 Application Proxies
Check the nginx service status and recent error logs on both nodes:
for host in rproxy-1 rproxy-2; do
echo "=== $host ===";
ssh $host "sudo systemctl status nginx; sudo tail -20 /var/log/nginx/error.log";
done
Test that each proxy node can reach the backend pool:
# Run from rproxy-1 or rproxy-2
curl -Isk http://192.168.20.151:8686
β‘ Note:
connect() failed (111: Connection refused)orupstream timed outin the error log means the backend is down or unreachable β go back to 0οΈβ£ Quick Triage.
β‘ Important: If one proxy node is healthy and the other is not, rproxy-0βs upstream block may still be sending traffic to the failed node. Check that nginx upstream health checks are active and that the failed nodeβs weight has not been manually set to 0.
5οΈβ£ Shared Path β Backend Services
The basic VM and service checks are in 0οΈβ£ Quick Triage. Use this section for deeper investigation on the backend host.
If the application is deployed using Docker, check if the container is running:
sudo docker ps -a
sudo docker logs --tail 100 <container>
If the application is deployed using a native service, check if the service is running:
sudo systemctl status <service> --no-pager
sudo journalctl -u <service> -n 100 --no-pager
Check the application health endpoint directly:
curl -s http://localhost:8686
Check for resource exhaustion:
uptime
free -h
df -h
6οΈβ£ Verify Service Restoration
After any fix is applied, confirm end-to-end functionality before closing the incident:
- Open your-domain.example.com in a browser from both an external device and an internal host.
- If OAuth2 is enabled for the service, navigate to a protected page and confirm the Entra ID login flow completes successfully.
- Confirm no 5xx errors appear in Cloudflare Analytics β Traffic (external path).
- Confirm Pi-hole query log shows clean resolution for your domain (internal path).
- Confirm rproxy-0 nginx access log shows 200s flowing through to the upstream nodes.
# External path β final check
curl -Isk https://lidarr.refol.us | head -5
# Internal path β final DNS check
dig lidarr.refol.us @192.168.20.253
# Confirm upstream traffic on rproxy-0
sudo tail -20 /var/log/nginx/access.log
β‘ Important: Validate both entry paths before closing. A fix that restores the external path does not guarantee the internal path is healthy, and vice versa.
β Notes
- Always start on the Ansible control node so remediation commands run in the correct environment.
- Run 0οΈβ£ Quick Triage first β confirm the backend VM is up and the service is running before investigating Cloudflare, Pi-hole, or the proxies. A down backend is the most common cause of a 502.
- Use the traffic flow diagram at the top of this page to identify which path is affected before starting.
- After triage, work outside-in on each path: Cloudflare β rproxy-0 for external; Pi-hole β rproxy-0 for internal.
- Both paths share the rproxy-0 β rproxy-1/2 β backend tier β a failure there affects all clients regardless of entry path.
- OAuth2 / Entra ID authentication is optional per service and runs at rproxy-0. Confirm whether it is enabled before investigating auth issues.
- Pi-hole IP 192.168.20.253 must be reachable.
- Always run
sudo nginx -tbefore anysystemctl reload nginxβ a bad config will take the proxy down. - Use
--check --diffon any Ansible playbook before applying changes during a live incident. - Log every action and timestamp in the incident ticket as you go β the post-mortem depends on it.