diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..4e6d780 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,416 @@ +# 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: + +```bash +ssh pi@hiypi.local +``` + +--- + +## 2. First boot — update and configure + +```bash +sudo apt update && sudo apt full-upgrade -y +sudo apt install -y git curl ufw fail2ban unattended-upgrades +``` + +### Static IP (optional but recommended) + +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 +``` + +```bash +sudo reboot +``` + +--- + +## 3. Harden SSH + +Disable password authentication so only your key can log in: + +```bash +sudo nano /etc/ssh/sshd_config +``` + +Set / verify: +``` +PasswordAuthentication no +PermitRootLogin no +``` + +```bash +sudo systemctl restart ssh +``` + +Test that you can still log in with your key before closing the current session. + +--- + +## 4. Firewall + +```bash +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: + +```bash +sudo systemctl enable fail2ban --now +``` + +--- + +## 5. Install Docker + +```bash +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` | `` | Proxied (orange cloud) | + | A | `*` | `` | Proxied (orange cloud) | + + The wildcard `*` record routes `.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 + +```bash +git clone https://github.com/YOUR_USER/Hostityourself.git ~/hiy-platform +cd ~/hiy-platform +``` + +--- + +## 8. Configure the platform + +Copy and edit the environment file: + +```bash +cp infra/.env.example infra/.env # if it doesn't exist, create it +nano infra/.env +``` + +Minimum required variables: + +```env +# Your domain (apps will be at .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: + +```json +{ + "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 + +```bash +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 URL** — `https://github.com/you/my-api.git` + - **Branch** — `main` + - **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) + +```bash +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/` | + | 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: + +```bash +sudo nano /etc/cron.daily/hiy-backup +``` + +```bash +#!/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" +``` + +```bash +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 + +```bash +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`: + +```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" +``` + +```bash +docker run -d --name gatus \ + -p 8080:8080 \ + -v ~/gatus:/config \ + twinproduction/gatus +``` + +--- + +## 15. Updating the platform itself + +```bash +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-` |