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
This commit is contained in:
parent
ec0f421137
commit
c7c4e7a2ec
1 changed files with 416 additions and 0 deletions
416
docs/setup.md
Normal file
416
docs/setup.md
Normal file
|
|
@ -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` | `<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
|
||||
|
||||
```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 <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:
|
||||
|
||||
```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/<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:
|
||||
|
||||
```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-<app-id>` |
|
||||
Loading…
Add table
Reference in a new issue