A modern web interface for managing Caddy reverse proxy hosts.
Add, edit and delete proxy hosts from a clean UI β let Caddy handle TLS, HTTP/2, HSTS and automatic certificates for you.
Warning
This project is under active development and not yet considered stable. Use at your own risk and review the configuration before exposing it publicly.
- Host management β create, edit and delete hosts with multiple domains and upstream backends.
- Two ways to drive Caddy, switchable at runtime:
- π Caddyfile mode (default) β renders a per-host Caddyfile snippet and reloads Caddy.
- π API mode β manages Caddy's JSON config through the admin API, one route per host, no reload required.
- Flexible authentication β local email/password or external OIDC/OAuth (Authelia, Keycloak, Authentik, β¦) with just-in-time user provisioning.
- Single binary β a modern SPA (Vite Β· React Β· TypeScript Β· Tailwind Β· shadcn/ui) embedded directly in the Go binary, or served from an external directory.
- Container-ready β multi-stage Docker image that bundles Caddy and starts everything for you.
docker run -d --name cpm \
-p 3001:3001 -p 80:80 -p 443:443 \
-v cpm-data:/data \
ghcr.io/pacerino/caddyproxymanagerThen open http://localhost:3001 and sign in with the default credentials:
| Password | |
|---|---|
admin@example.com |
changeme |
Important
Change the defaults via CPM_ADMIN_EMAIL / CPM_ADMIN_PASSWORD on first run.
The container starts Caddy (with its admin API) and CPM in api mode. Mount your own Caddy config at /data/caddy.json to customise it.
Images are published for linux/amd64 and linux/arm64 in two flavors:
| Tag | Contents |
|---|---|
latest, <version>, <major>.<minor> |
Newest stable release with the stock Caddy build. |
latest-dns, <version>-dns, β¦ |
Same, with Caddy built with 25 DNS providers for ACME DNS-01 challenges (see DNS providers). |
Note
Images are only published for version tags. Pre-releases (e.g. v3.2.0-rc1) get their exact version tag but never move latest.
ββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββ
β Browser β ββββΊ β CPM (Go + embedded β ββββΊ β Caddy β
β (React SPA) β β React frontend) β β (reverse proxy) β
ββββββββββββββββ ββββββββββββ¬ββββββββββββ βββββββββββββββββββ
β
Caddyfile mode β writes host_<id>.conf + reload
API mode β PUT/PATCH/DELETE via admin API
CPM stores hosts in a local SQLite database and applies each change to Caddy using the selected provider.
All settings are environment variables prefixed with CPM_.
| Variable | Default | Description |
|---|---|---|
CPM_CADDY_MODE |
caddyfile |
Provider to use: caddyfile or api. |
CPM_CADDY_ADMINURL |
http://localhost:2019 |
Caddy admin API base URL (API mode + api reload). |
CPM_CADDY_SERVERNAME |
srv0 |
HTTP server name routes are managed under (API mode). |
CPM_CADDY_LISTEN |
:80,:443 |
Listen addresses for the bootstrapped server (API mode); use e.g. :8080 for local non-root testing. |
CPM_CADDY_RELOADSTRATEGY |
systemd |
Reload strategy (Caddyfile mode): systemd, exec, api or none. |
CPM_CADDY_BINARY |
caddy |
Caddy executable for the exec reload strategy. |
CPM_CADDY_SERVICE |
caddy.service |
systemd unit for the systemd reload strategy. |
CPM_CADDY_SUPERVISE |
false (true in the Docker image) |
CPM starts and supervises the Caddy process itself (API mode); required for installing plugins in Docker. |
CPM_CADDY_CONFIGFILE |
<CPM_DATAFOLDER>/caddy.json |
JSON config the supervised Caddy starts with. |
CPM_CADDYFILE |
/etc/caddy/Caddyfile |
Main Caddyfile (used by exec/api reload). |
CPM_CADDY_SCHEMAFILE |
/usr/share/cpm/caddy_schema.json |
Caddy JSON schema used to describe DNS provider fields (API mode). |
| Variable | Default | Description |
|---|---|---|
CPM_AUTH_MODE |
local |
local or oidc. |
CPM_AUTH_OIDC_ISSUER |
β | OIDC issuer URL (e.g. Keycloak/Authelia). |
CPM_AUTH_OIDC_CLIENTID |
β | OIDC client ID. |
CPM_AUTH_OIDC_CLIENTSECRET |
β | OIDC client secret. |
CPM_AUTH_OIDC_REDIRECTURL |
β | Callback URL, e.g. https://cpm.example.com/api/auth/oidc/callback. |
CPM_AUTH_OIDC_SCOPES |
openid,profile,email |
Requested scopes. |
CPM_AUTH_OIDC_ALLOWEDDOMAINS |
β | Optional email-domain allowlist for JIT provisioning. |
| Variable | Default | Description |
|---|---|---|
CPM_DATAFOLDER |
/etc/caddy/ |
Data + per-host config folder. |
CPM_LOGFOLDER |
/var/log/caddy |
Per-host log folder. |
CPM_PLUGIN_INSTALL |
true |
Allow installing and removing Caddy plugins from the UI. Set to false to make the installed list read-only. |
CPM_FRONTENDDIR |
β | Serve the frontend from this directory instead of the embedded assets. |
CPM_ADMIN_EMAIL |
admin@example.com |
Seed admin email (first run, local mode). |
CPM_ADMIN_PASSWORD |
changeme |
Seed admin password (first run, local mode). |
Prerequisites: Go 1.24+, Node 22+, and a local Caddy for end-to-end testing.
Run the backend and frontend in two terminals:
# Terminal 1 β backend on :3001
cd backend
CPM_DATAFOLDER=./data CPM_LOGFOLDER=./data CPM_CADDY_RELOADSTRATEGY=none \
go run ./cmd/main.go
# Terminal 2 β frontend dev server on :5173 (proxies /api β :3001)
cd frontend
npm install
npm run devOpen http://localhost:5173 and log in with admin@example.com / changeme.
cd frontend && npm run build # emits into backend/embed/assets
cd ../backend && go build ./cmd/main.gocd backend && go test ./... # backend
cd frontend && npm run test # frontend unit tests
cd frontend && npm run build # frontend type-check + buildCI (.github/workflows/ci.yml) runs on GitHub-hosted runners. It runs the backend and frontend tests, and builds the multi-arch Docker images (both flavors) on every PR and push to master. Nothing is published automatically: only pushing a version tag (v*) publishes the images to GHCR and creates a GitHub release with cross-compiled binaries.
docker build -t caddyproxymanager .The multi-stage build compiles the frontend and backend (a pure-Go build, no C toolchain required) into a small Alpine runtime that bundles Caddy. Caddy is always built with xcaddy, including caddy-json-schema, and the image ships the generated schema at /usr/share/cpm/caddy_schema.json. The build needs internet access (the schema generation fetches the Caddy docs).
To bundle Caddy plugins, pass their Go module paths (optionally pinned with @version) and they are added to the xcaddy build on top of caddy-json-schema:
docker build -t caddyproxymanager:custom \
--build-arg CADDY_PLUGINS="github.com/caddy-dns/cloudflare github.com/mholt/caddy-ratelimit" .CADDY_VERSION (default 2.11.4) selects the Caddy release.
Should I use Caddyfile mode or API mode?
Use Caddyfile mode (the default) for the simplest setup β CPM writes snippets that Caddy imports, and you keep all of Caddy's conveniences (automatic HTTPS, HSTS, HTTP/2). Use API mode when you want CPM to manage Caddy's live JSON config directly through the admin API, with no reloads.
Can I use external SSO (Authelia, Keycloak, Authentik β¦)?
Yes. Set CPM_AUTH_MODE=oidc and configure the CPM_AUTH_OIDC_* variables. On first successful login CPM provisions the matching user automatically (JIT). Restrict access with CPM_AUTH_OIDC_ALLOWEDDOMAINS.
Does CPM run Caddy for me?
The Docker image does β CPM starts and supervises Caddy itself (restarting it if it crashes) and keeps its data in /data. For bare-metal installs, run Caddy yourself and point CPM at it (CPM_CADDYFILE / CPM_CADDY_ADMINURL) with the appropriate reload strategy.
CPM can obtain certificates through ACME DNS-01 challenges, e.g. for wildcard domains or hosts that are not reachable from the internet.
- Open the Plugins page and save credentials for a DNS provider (Cloudflare, DuckDNS, Porkbun, or any other DNS module built into your Caddy).
- Pick one of the saved providers when creating or editing a host.
Use Caddy {env.VAR} placeholders for secrets (e.g. api_token = {env.DUCKDNS_TOKEN}) and pass the variable to the container (-e DUCKDNS_TOKEN=...). The placeholder is stored in the database, not the secret itself, and API responses never contain secret values.
The -dns image flavor bundles these providers: cloudflare, duckdns, porkbun, route53, digitalocean, hetzner, ovh, namecheap, gandi, desec, netcup, ionos, linode, godaddy, rfc2136, acmedns, bunny, inwx, azure, googleclouddns, namesilo, powerdns, transip, alidns, he. For other providers, build your own image with CADDY_PLUGINS (see Building the image yourself).
Note
Hosts using a DNS provider get a dedicated TLS policy that uses Let's Encrypt only; there is no ZeroSSL fallback for DNS-challenge hosts.
On the Plugins page you can add or remove Caddy plugins without rebuilding the image. CPM asks the Caddy download service for a build containing the plugin, swaps Caddy over to it and rolls back automatically if Caddy does not come up healthy.
- Any registered plugin works. Search the catalog by package path or module name (e.g.
ratelimitfindsgithub.com/mholt/caddy-ratelimit) and pick a plugin from the results. Plugins from the tested list (the ones bundled in the-dnsimage) are marked as tested; everything else is marked untested, so use it at your own risk. - Caddy version. Caddy's build service always builds the latest Caddy release, so installing or removing a plugin also updates Caddy if the image ships an older one (CPM never downgrades).
- Restart note. Switching binaries restarts Caddy, so there is a short interruption while the new build starts. Existing plugins stay pinned to their installed version; the new one is added at its latest version.
- Rollback. CPM keeps the previous binary. If the new build fails to start, or applying your hosts to it reports errors, CPM switches back automatically and shows the error.
- Updates. When the image ships a newer Caddy than the managed binary runs (e.g. after pulling a new CPM image), the Plugins page shows an update banner and offers to rebuild your plugins on that Caddy version (the same safe switch with rollback). Turn on Update automatically to do this at startup. A failed update is reported on the page.
- Failures stay visible. A failed or rolled-back change, or CPM switching back to the shipped Caddy because the managed binary could not start, is shown on the Plugins page until a later change succeeds.
- Disabling. Set
CPM_PLUGIN_INSTALL=falseto turn installing off; the installed list stays visible but read-only. - Internet access is required to download builds. For offline setups use the
-dnsimage flavor (or build your own withCADDY_PLUGINS) and keep plugin install disabled. - Requirements. API mode (
CPM_CADDY_MODE=api) is required. The Docker image works out of the box because CPM supervises Caddy itself (CPM_CADDY_SUPERVISE=true); Caddy's certificates and autosave are stored in the/datavolume. Managed binaries live in<CPM_DATAFOLDER>/caddy-bin/. CPM can only change a Caddy it runs or restarts on the same machine: withCPM_CADDY_ADMINURLpointing at another host or container, plugin install is disabled and the Plugins page says why.
Without Docker, Caddy runs under systemd and CPM can only restart the service, so the unit must start CPM's managed binary. CPM never touches the system binary (e.g. /usr/bin/caddy): when plugin install is enabled (CPM_PLUGIN_INSTALL, the default), CPM copies the configured Caddy on start (CPM_CADDY_BINARY) into <CPM_DATAFOLDER>/caddy-bin/ and makes that copy the managed binary (caddy-bin/current), so the unit keeps running the same Caddy after the switch. The copy is kept as the seed: whenever CPM has to fall back, it points current at the seed again instead of removing it.
The Plugins page then shows the exact drop-in to install; it looks like this (adjust the path to your CPM_DATAFOLDER, keep your original arguments):
# systemctl edit caddy.service
[Service]
ExecStart=
ExecStart=/etc/caddy/caddy-bin/current run --environ --config /etc/caddy/CaddyfileThen run systemctl daemon-reload && systemctl restart caddy. The service user must be able to read the managed binary and CPM needs permission to restart the unit. Run CPM with CPM_CADDY_RELOADSTRATEGY=systemd (the default).
When your distribution updates its Caddy package, the managed binary stays on the copied version; the Plugins page then offers the update to the newer Caddy.
-
Docker: remove the managed binary link and restart the container; CPM then runs the Caddy that ships with the image:
docker exec cpm rm /data/caddy-bin/current docker restart cpmCPM also does this by itself when the managed binary cannot start (for example after moving
/datato a machine with another CPU architecture). -
systemd: point
currentback at the seed (the copy of your original Caddy) and restart the service:cd <CPM_DATAFOLDER>/caddy-bin && ln -sfn "$(readlink seed)" current && rm -f previous systemctl restart caddy
To leave plugin management entirely, remove the drop-in (
systemctl revert caddy.service) and restart Caddy.
Images with plugin install keep Caddy's data under /data: XDG_DATA_HOME=/data/.local/share and XDG_CONFIG_HOME=/data/.config. Older images stored certificates under /root/.local/share/caddy (lost on container re-creation unless you mounted it) and the autosave under /root/.config/caddy. If you mounted either path, copy its contents into the data volume once, then drop the old mounts:
docker run --rm -v <old-certs-volume>:/old-data -v <old-config-volume>:/old-config -v cpm-data:/data alpine \
sh -c 'mkdir -p /data/.local/share/caddy /data/.config/caddy && cp -a /old-data/. /data/.local/share/caddy/ && cp -a /old-config/. /data/.config/caddy/'Leave out the /old-config parts if you only mounted the certificates. Without the migration Caddy simply obtains new certificates (mind the Let's Encrypt rate limits for many domains).
- Plugins and DNS providers require
CPM_CADDY_MODE=api. In Caddyfile mode the plugin and DNS provider endpoints return400, and creating or updating a host with a DNS provider is rejected with400("DNS providers are only available when CPM_CADDY_MODE=api"). Provider choices saved earlier in API mode are kept but ignored, and the Hosts page flags them.
- Log viewer
- Per-host advanced directives
Inspired by the excellent Nginx Proxy Manager by jc21 β go check it out. CPM brings the same idea to Caddy.
Pull requests and issues are welcome! Please open an issue to discuss larger changes first.