9.3 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 podman python3 pipx aardvark-dns sqlite3
pipx install podman-compose
pipx ensurepath
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
You need a domain pointing to your Pi's public IP. Any DNS provider works.
-
Add DNS A records pointing to your public home IP:
Type Name Content A hiy<your-public-IP>A *<your-public-IP>The wildcard
*record routes<appname>.yourdomain.comto the Pi.
Port forwarding
Forward the following ports on your router to the Pi's static IP. Both ports are required — Caddy uses port 80 to prove domain ownership to Let's Encrypt before issuing the HTTPS certificate.
| External | Internal |
|---|---|
| 80/tcp | 80 |
| 443/tcp | 443 |
7. Clone the platform
git clone https://github.com/shautvast/Hostityourself.git ~/hiy-platform
cd ~/hiy-platform
8. Configure the platform
Copy and edit the environment file:
cp .env.example .env # if it doesn't exist, create it
nano .env
Minimum required variables:
# Your domain (apps will be at <name>.yourdomain.com)
DOMAIN_SUFFIX=yourdomain.com
# Email for Let's Encrypt expiry notices
ACME_EMAIL=you@example.com
9. Caddy — automatic HTTPS
Caddy obtains a Let's Encrypt certificate automatically via the HTTP-01 challenge. No DNS API token or Cloudflare account required.
The proxy/Caddyfile is already configured — nothing to edit here.
Just make sure DOMAIN_SUFFIX and ACME_EMAIL are set in infra/.env
and that ports 80 and 443 are forwarded to the Pi (see step 6).
10. Start the platform
cd ~/hiy-platform
./infra/start.sh
Open the dashboard: https://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/json| | Secret | (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 | Port 80/443 not forwarded or DNS not propagated yet | 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> |