# 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 > **Note:** Do **not** run `apt install docker-ce` directly — that package > requires Docker's own apt repository to be added first. > Use one of the two methods below. **Option A — Official convenience script (recommended, installs latest Docker CE):** ```bash curl -fsSL https://get.docker.com | sh ``` **Option B — Debian-packaged `docker.io` (older but simpler, no extra repo needed):** ```bash sudo apt install -y docker.io docker-compose ``` > **Note:** `docker.io` ships with Compose v1 as a separate binary. > Use `docker-compose` (with a hyphen) instead of `docker compose` everywhere in this guide. Then, regardless of which option you used: ```bash 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-` |