A lightweight, self-hosted Caddyfile manager with a modern web UI and GitHub sync.
Pebble is a homelab-focused tool for managing Caddy reverse proxy configurations. It bundles a Go backend, the Caddy binary, and a Svelte 5 single-page app into a single container — no external databases, no separate config repos needed out of the box.
⌘S / Ctrl+S to save & reload).import directives.cloudflared for exposing services without port forwarding.services:
pebble:
image: ghcr.io/lucawahlen/pebble:latest
ports:
- "80:80"
- "443:443"
- "3000:3000"
volumes:
- pebble-config:/etc/pebble
- caddy-files:/etc/caddy
restart: unless-stopped
volumes:
pebble-config:
caddy-files:
docker compose up -d
Open http://localhost:3000 to access the editor.
Port 80/443 are for Caddy to serve your actual sites. Port 3000 is the Pebble management UI.
docker run -d \
--name pebble \
-p 80:80 -p 443:443 -p 3000:3000 \
-v pebble-config:/etc/pebble \
-v caddy-files:/etc/caddy \
--restart unless-stopped \
ghcr.io/lucawahlen/pebble:latest
Two separate packages are published from a single multi-target Dockerfile:
| Image | Description |
|---|---|
ghcr.io/lucawahlen/pebble |
Base image — Pebble + Caddy |
ghcr.io/lucawahlen/pebble-tunnel |
Adds Cloudflare Tunnel (cloudflared) |
Both share the same version tags. Use whichever fits your setup.
# Base image
docker build --target pebble -t pebble .
# Tunnel variant
docker build --target pebble-tunnel -t pebble-tunnel .
All configuration is done through environment variables. Settings can also be changed at runtime through the UI's settings panel.
| Variable | Default | Description |
|---|---|---|
PEBBLE_PASSWORD |
— | Password for the management UI. If unset, a setup screen prompts on first visit |
DISABLE_AUTH |
false |
Set to true to completely disable authentication (use only in trusted networks) |
PORT |
3000 |
Port for the Pebble management UI |
HOST |
0.0.0.0 |
Bind address |
CADDYFILES_DIR |
/etc/caddy |
Directory for Caddy config files |
PEBBLE_CONFIG |
/etc/pebble/config.json |
Path to the Pebble settings file |
GITHUB_TOKEN |
— | GitHub Personal Access Token |
GITHUB_REPO |
— | GitHub repository (owner/repo) |
GITHUB_BRANCH |
main |
Branch to sync with |
SYNC_ENABLED |
true |
Enable/disable automatic GitHub sync |
TUNNEL_TOKEN |
— | Cloudflare Tunnel token (tunnel image only) |
| Path | Purpose |
|---|---|
/etc/caddy |
Caddy configuration files (your Caddyfiles) |
/etc/pebble |
Pebble settings (GitHub config, persisted across restarts) |
Pebble requires a password to access the management UI and all API endpoints.
There are two ways to configure the password:
Option 1: Environment variable — Set PEBBLE_PASSWORD before starting the container:
environment:
- PEBBLE_PASSWORD=your-secure-password
Option 2: UI setup — If no PEBBLE_PASSWORD is set, the first visit to the UI presents a setup screen where you create a password (minimum 8 characters). The password is persisted to the config file and survives container restarts.
/api/* endpoints (except /api/auth/*) require a valid session.HttpOnly, SameSite=Strict cookies that expire after 7 days.401, the UI automatically shows the login screen./health endpoint and the /welcome page remain public.| Endpoint | Method | Description |
|---|---|---|
/api/auth/check |
GET |
Returns {authenticated, authRequired, needsSetup} |
/api/auth/setup |
POST |
Set initial password ({"password": "..."}) — only works once |
/api/auth/login |
POST |
Login ({"password": "..."}) — returns session cookie |
/api/auth/logout |
POST |
Invalidates session and clears cookie |
Connect a GitHub repository to version-control your Caddy configuration and sync it across instances.
repo scopeContents: Read and Write permissionowner/repo), and branch.You can also trigger pull/push manually from the settings panel.
Environment variables fill in empty fields on first load only. Once you save settings through the UI, those values are persisted and take precedence. SYNC_ENABLED only applies when no config file exists yet. PEBBLE_PASSWORD always takes priority over a UI-set password when both are present.
The pebble-tunnel image includes cloudflared for exposing services without opening ports on your router.
services:
pebble:
image: ghcr.io/lucawahlen/pebble-tunnel:latest
ports:
- "3000:3000"
- "80:80"
- "443:443"
environment:
- TUNNEL_TOKEN=your-tunnel-token-here
volumes:
- pebble-config:/etc/pebble
- caddy-files:/etc/caddy
restart: unless-stopped
volumes:
pebble-config:
caddy-files:
Get your tunnel token from the Cloudflare Zero Trust dashboard under Networks → Tunnels.
Pebble exposes a GET /health endpoint that returns {"status":"ok"}. The Docker images include a built-in HEALTHCHECK that uses this endpoint.
┌────────────────────────────────────────────────┐
│ Docker Container │
│ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ Pebble Server│ │ Caddy │ │
│ │ (Go + SPA) │◄──►│ (reverse proxy) │ │
│ │ :3000 │ │ :80 / :443 │ │
│ └──────┬───────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ /etc/caddy/ Your configured sites │
│ (Caddyfiles) │
│ │
│ ┌──────────────────┐ ┌───────────────────┐ │
│ │ GitHub Sync │ │ cloudflared │ │
│ │ (poll every 60s)│ │ (tunnel image) │ │
│ └──────────────────┘ └───────────────────┘ │
└────────────────────────────────────────────────┘
Tech stack:
net/http, embedded static assetsdistroless/static-debian12 basecd ui
npm install
npm run dev
The dev server runs on http://localhost:5173 with HMR.
cd server
# Copy the UI build into the expected embed directory
mkdir -p static && cp -r ../ui/build/* static/
go run .
The server starts on http://localhost:3000.