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
416 lines
9.6 KiB
Markdown
416 lines
9.6 KiB
Markdown
# 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>` |
|