Hostityourself/docs/setup.md
Claude c7c4e7a2ec
docs: add Raspberry Pi end-to-end setup guide
Covers: OS flash, SSH hardening, ufw/fail2ban, Docker install,
Cloudflare DNS + wildcard TLS, platform startup via docker compose,
first app deploy, webhook setup, daily backups, Netdata/Gatus monitoring,
platform upgrades, and a troubleshooting table.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-19 13:29:41 +00:00

9.6 KiB

HostItYourself — Raspberry Pi Setup Guide

End-to-end guide to turn a fresh Raspberry Pi into a self-hosted PaaS. After completing this guide you will have:

  • Automatic HTTPS for every deployed app via Caddy + Cloudflare
  • git push → live deploys triggered by GitHub webhooks
  • A web dashboard at hiy.yourdomain.com
  • Firewall, fail2ban, and SSH key-only hardening

Hardware

Component Minimum Recommended
Pi model Raspberry Pi 4 4 GB Raspberry Pi 4/5 8 GB
Storage 32 GB microSD (A2) 64 GB+ USB SSD
Network Wi-Fi Wired Ethernet
Power Official USB-C PSU PSU + UPS hat

A USB SSD is strongly preferred — builds involve a lot of small file I/O that kills microSD cards within months.


1. Install Raspberry Pi OS

  1. Download Raspberry Pi Imager and flash Raspberry Pi OS Lite (64-bit).
  2. Before writing, click the gear icon (⚙) and:
    • Enable SSH with a public-key (paste your ~/.ssh/id_ed25519.pub)
    • Set a hostname, e.g. hiypi
    • Set your Wi-Fi credentials (or leave blank for wired)
  3. Insert the SD/SSD and boot the Pi.

Verify you can SSH in:

ssh pi@hiypi.local

2. First boot — update and configure

sudo apt update && sudo apt full-upgrade -y
sudo apt install -y git curl ufw fail2ban unattended-upgrades

Edit /etc/dhcpcd.conf and add at the bottom (adjust for your network):

interface eth0
static ip_address=192.168.1.50/24
static routers=192.168.1.1
static domain_name_servers=1.1.1.1 8.8.8.8
sudo reboot

3. Harden SSH

Disable password authentication so only your key can log in:

sudo nano /etc/ssh/sshd_config

Set / verify:

PasswordAuthentication no
PermitRootLogin no
sudo systemctl restart ssh

Test that you can still log in with your key before closing the current session.


4. Firewall

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP (redirect)
sudo ufw allow 443/tcp   # HTTPS
sudo ufw enable
sudo ufw status

fail2ban

The default config already jails repeated SSH failures. Restart to apply:

sudo systemctl enable fail2ban --now

5. Install Docker

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker pi   # allow running docker without sudo
newgrp docker                # apply group without logging out
docker run --rm hello-world  # verify

6. Domain and DNS (Cloudflare)

You need a domain whose DNS is managed by Cloudflare (the free plan is fine).

  1. In Cloudflare, add two DNS records pointing to your public home IP:

    Type Name Content Proxy
    A hiy <your-public-IP> Proxied (orange cloud)
    A * <your-public-IP> Proxied (orange cloud)

    The wildcard * record routes <appname>.yourdomain.com to the Pi.

  2. In Cloudflare → SSL/TLS → Overview, set mode to Full (strict).

  3. Create a Cloudflare API token for Caddy's ACME DNS challenge:

    • Go to My Profile → API Tokens → Create Token
    • Use the Edit zone DNS template, scope it to your zone
    • Copy the token — you will need it in step 8

Port forwarding

Forward the following ports on your router to the Pi's static IP:

External Internal
80/tcp 80
443/tcp 443

7. Clone the platform

git clone https://github.com/YOUR_USER/Hostityourself.git ~/hiy-platform
cd ~/hiy-platform

8. Configure the platform

Copy and edit the environment file:

cp infra/.env.example infra/.env   # if it doesn't exist, create it
nano infra/.env

Minimum required variables:

# Your domain (apps will be at <name>.yourdomain.com)
DOMAIN_SUFFIX=yourdomain.com

# Cloudflare API token for ACME DNS-01 challenge
CF_API_TOKEN=your_cloudflare_api_token

9. Caddy — automatic HTTPS

Caddy handles TLS via Cloudflare's DNS challenge (no port-80 HTTP challenge needed, works even behind CGNAT).

Edit proxy/caddy.json and replace the top-level config with:

{
  "admin": { "listen": "0.0.0.0:2019" },
  "apps": {
    "tls": {
      "automation": {
        "policies": [{
          "subjects": ["*.yourdomain.com", "yourdomain.com"],
          "issuers": [{
            "module": "acme",
            "challenges": {
              "dns": {
                "provider": {
                  "name": "cloudflare",
                  "api_token": "{env.CF_API_TOKEN}"
                }
              }
            }
          }]
        }]
      }
    },
    "http": {
      "servers": {
        "hiy": {
          "listen": [":80", ":443"],
          "routes": [
            {
              "handle": [{
                "handler": "reverse_proxy",
                "upstreams": [{"dial": "server:3000"}]
              }]
            }
          ]
        }
      }
    }
  }
}

Note: The Caddy image in docker-compose.yml needs the Cloudflare DNS plugin. Use caddy:2-alpine with xcaddy or replace the image with ghcr.io/caddybuilds/caddy-cloudflare:latest.


10. Start the platform

cd ~/hiy-platform
docker compose --env-file infra/.env up -d --build
docker compose logs -f   # watch startup

When ready you should see:

hiy-server  | Listening on http://0.0.0.0:3000

Open the dashboard: https://hiy.yourdomain.com


11. Deploy your first app

From the dashboard

  1. Open https://hiy.yourdomain.com
  2. Fill in the Add App form:
    • Name — a URL-safe slug, e.g. my-api
    • GitHub Repo URLhttps://github.com/you/my-api.git
    • Branchmain
    • Container Port — the port your app listens on (e.g. 3000)
  3. Click Create App, then Deploy on the new row.
  4. Watch the build log in the app detail page.
  5. Once done, your app is live at https://my-api.yourdomain.com.

Build strategy detection

The build engine picks a strategy automatically:

What's in the repo Strategy
Dockerfile docker build
package.json / yarn.lock Cloud Native Buildpack (Node)
requirements.txt / pyproject.toml Cloud Native Buildpack (Python)
go.mod Cloud Native Buildpack (Go)
static/ or public/ directory Caddy static file server

A Dockerfile always takes precedence. For buildpack strategies, the pack CLI must be installed on the Pi (see below).

Install pack (for buildpack projects)

curl -sSL https://github.com/buildpacks/pack/releases/latest/download/pack-linux-arm64.tgz \
  | sudo tar -xz -C /usr/local/bin
pack --version

12. Set up GitHub webhooks (auto-deploy on push)

For each app:

  1. Go to the app detail page → GitHub Webhook section. Copy the Payload URL and Secret.

  2. Open the GitHub repo → Settings → Webhooks → Add webhook:

    Field Value
    Payload URL https://hiy.yourdomain.com/webhook/<app-id>
    Content type application/json
    Secret (from the app detail page)
    Events Just the push event
  3. Click Add webhook. GitHub will send a ping — check Recent Deliveries for a green tick.

After this, every git push to the configured branch triggers a deploy.


13. Backups

Create a daily backup cron job:

sudo nano /etc/cron.daily/hiy-backup
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR=/var/backups/hiy
DATE=$(date +%Y%m%d)
mkdir -p "$BACKUP_DIR"

# Database
docker exec hiy-platform-server-1 sh -c \
  'sqlite3 /data/hiy.db .dump' > "$BACKUP_DIR/db-$DATE.sql"

# Env files
tar -czf "$BACKUP_DIR/env-$DATE.tar.gz" \
  -C /var/lib/docker/volumes/hiy-platform_hiy-data/_data env/ 2>/dev/null || true

# Keep 30 days
find "$BACKUP_DIR" -mtime +30 -delete

echo "HIY backup complete: $BACKUP_DIR"
sudo chmod +x /etc/cron.daily/hiy-backup

To copy backups off-Pi, add an rclone copy line pointing to S3, Backblaze B2, or any rclone remote.


14. Monitoring (optional)

Netdata — per-container resource metrics

curl https://my-netdata.io/kickstart.sh | bash

Access at http://hiypi.local:19999 or add a Caddy route for netdata.yourdomain.com.

Gatus — HTTP uptime checks

Create ~/gatus/config.yaml:

endpoints:
  - name: my-api
    url: https://my-api.yourdomain.com/health
    interval: 1m
    conditions:
      - "[STATUS] == 200"
    alerts:
      - type: email
        description: "my-api is down"
docker run -d --name gatus \
  -p 8080:8080 \
  -v ~/gatus:/config \
  twinproduction/gatus

15. Updating the platform itself

cd ~/hiy-platform
git pull origin main
docker compose --env-file infra/.env up -d --build

Zero-downtime: the old containers keep serving traffic while the new ones build. Caddy never restarts.


Troubleshooting

Symptom Likely cause Fix
Dashboard not reachable Compose not started / port 443 not forwarded docker compose logs server
TLS certificate error CF_API_TOKEN wrong or DNS not propagated Check Caddy logs: docker compose logs caddy
Build fails — "docker not found" Docker socket not mounted Verify docker-proxy service is up
Build fails — "pack not found" pack not installed See step 11
Webhook returns 401 Secret mismatch Regenerate app, re-copy secret to GitHub
Webhook returns 404 Wrong app ID in URL Check app ID on the dashboard
App runs but 502 from browser Container port wrong Check Container Port matches what the app listens on
Container shows "exited" App crashed on startup docker logs hiy-<app-id>