10 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 → livedeploys 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
- Download Raspberry Pi Imager and flash Raspberry Pi OS Lite (64-bit).
- 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)
- Enable SSH with a public-key (paste your
- 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
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
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
Note: Do not run
apt install docker-cedirectly — 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):
curl -fsSL https://get.docker.com | sh
Option B — Debian-packaged docker.io (older but simpler, no extra repo needed):
sudo apt install -y docker.io docker-compose
Note:
docker.ioships with Compose v1 as a separate binary. Usedocker-compose(with a hyphen) instead ofdocker composeeverywhere in this guide.
Then, regardless of which option you used:
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).
-
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.comto the Pi. -
In Cloudflare → SSL/TLS → Overview, set mode to Full (strict).
-
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.ymlneeds the Cloudflare DNS plugin. Usecaddy:2-alpinewithxcaddyor replace the image withghcr.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
- Open
https://hiy.yourdomain.com - 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)
- Name — a URL-safe slug, e.g.
- Click Create App, then Deploy on the new row.
- Watch the build log in the app detail page.
- 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:
-
Go to the app detail page → GitHub Webhook section. Copy the Payload URL and Secret.
-
Open the GitHub repo → Settings → Webhooks → Add webhook:
Field Value Payload URL https://hiy.yourdomain.com/webhook/<app-id>Content type application/jsonSecret (from the app detail page) Events Just the push event -
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> |