PerryLabs Homelab Runbook

Last updated: 2025-08-10 14:29

Overview

Homelab on two Raspberry Pis:

Secrets are redacted. Replace <REDACTED> placeholders with your own values.

DNS

Public zone: Cloudflare (perrylabs.cc). Public apps published via Cloudflare Tunnel.

LAN: Pi-hole provides *.home.perrylabs.cc A-records pointing to NPM (192.168.68.76).

Nginx Proxy Manager

Host: 192.168.68.76   Compose: ~/nginxproxymanager/compose.yaml

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped
    ports:
      - "80:80"
      - "81:81"
      - "443:443"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks: [edge]
networks:
  edge:
    external: true

For each proxy host: enable Block common exploits, Websockets (if needed), HTTP/2, cert *.home.perrylabs.cc, and Force SSL.

Cloudflared Tunnel

Host: 192.168.68.76   Compose: ~/cloudflared/compose.yaml

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel run
    environment:
      - TUNNEL_TOKEN=<REDACTED_TUNNEL_TOKEN>
    networks: [edge]
networks:
  edge:
    external: true

Public hostnames in the tunnel point to origin https://npm:443, with No TLS Verify ON and both Host header and SNI set to the public hostname.

Service map

ServiceLANPublicNPM → UpstreamNotes
Grafanahttps://grafana.home.perrylabs.cchttps://grafana.perrylabs.cc192.168.68.56:3000HTTP/2, WS, Force SSL
Portainerhttps://portainer.home.perrylabs.cchttps://portainer.perrylabs.cc192.168.68.76:9000WS
n8nhttps://n8n.home.perrylabs.cchttps://n8n.perrylabs.cc192.168.68.56:5678Basic Auth in app
Pi-hole #1https://pihole1.home.perrylabs.cchttps://pihole1.perrylabs.cc192.168.68.76:8082Access protected
Pi-hole #2https://pihole2.home.perrylabs.cchttps://pihole2.perrylabs.cc192.168.68.56:8082Access protected
NPM UIhttps://npm.home.perrylabs.cchttps://npm.perrylabs.cc192.168.68.76:81Access protected
Dozzlehttps://dozzle.home.perrylabs.cchttps://dozzle.perrylabs.cc192.168.68.56:8083Optional NPM Basic

Monitoring

Host: 192.168.68.56.

Prometheus config

global:
  scrape_interval: 30s
  evaluation_interval: 30s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['192.168.68.56:9090']
        labels: { host: 'rpi-400' }
    relabel_configs:
      - source_labels: [host]
        target_label: instance

  - job_name: node
    static_configs:
      - targets: ['192.168.68.56:9100']
        labels: { host: 'rpi-400' }
      - targets: ['192.168.68.76:9100']
        labels: { host: 'rpi-nginx' }
    relabel_configs:
      - source_labels: [host]
        target_label: instance

  - job_name: cadvisor
    static_configs:
      - targets: ['192.168.68.56:8085']
        labels: { host: 'rpi-400' }
      - targets: ['192.168.68.76:8080']
        labels: { host: 'rpi-nginx' }
    relabel_configs:
      - source_labels: [host]
        target_label: instance

Dashboards

n8n

Host: 192.168.68.56   Compose: ~/containers/n8n/compose.yaml

Final configuration with named volume, Basic Auth, task runners, and settings file permission enforcement. Secrets are redacted below.

services:
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    platform: linux/arm64
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      TZ: Australia/Sydney
      WEBHOOK_URL: https://n8n.perrylabs.cc
      N8N_PROTOCOL: https
      N8N_PORT: "5678"
      N8N_BASIC_AUTH_ACTIVE: "true"
      N8N_BASIC_AUTH_USER: admin
      N8N_BASIC_AUTH_PASSWORD: "<REDACTED_PASSWORD>"
      N8N_ENCRYPTION_KEY: "<REDACTED_ENCRYPTION_KEY>"
      N8N_RUNNERS_ENABLED: "true"
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
    volumes:
      - n8n_data:/home/node/.n8n

volumes:
  n8n_data: {}

Notes: Do not set N8N_EDITOR_BASE_URL or N8N_HOST so the UI works for both LAN and public hostnames. Use WEBHOOK_URL for external callbacks.

Enable memory cgroups on Raspberry Pi

1) Add kernel flags (single-line file)

[ -f /boot/firmware/cmdline.txt ] && CMD=/boot/firmware/cmdline.txt || CMD=/boot/cmdline.txt
echo "Using $CMD"
sudo cp "$CMD" "$CMD.bak.$(date +%F-%H%M)"
grep -q 'cgroup_enable=memory' "$CMD" || \
  sudo sed -i 's/$/ cgroup_memory=1 cgroup_enable=memory cgroup_enable=cpuset/' "$CMD"
sudo reboot

2) Verify after reboot

cat /sys/fs/cgroup/cgroup.controllers | tr ' ' '\n' | grep -x memory && echo "memory controller present"

3) cAdvisor mount

volumes:
  - /sys/fs/cgroup:/sys/fs/cgroup:ro

Dozzle

Compose: ~/containers/dozzle/compose.yaml

services:
  dozzle:
    image: amir20/dozzle:latest
    container_name: dozzle
    restart: unless-stopped
    ports: ["8083:8080"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - DOZZLE_ADDR=0.0.0.0:8080

Pi-hole

Each Pi-hole maps 8082:80 (web) and persists /etc/pihole and /etc/dnsmasq.d. Add local DNS records for each *.home.perrylabs.cc host → 192.168.68.76.

Security

Access (per app)

Self-hosted apps protected by email OTP. Apply to Grafana, Portainer, n8n, Dozzle, NPM UI, Pi-holes.

WAF: block non-AU

(http.host eq "perrylabs.cc" or ends_with(http.host, ".perrylabs.cc"))
and not ip.geoip.country in {"AU"}

Ops