Note
This is a fork of fuomag9/caddy-proxy-manager. The section below documents changes made in this fork. The original upstream README is collapsed at the bottom of the page.
The Caddy image includes a vendored copy of fuomag9/caddy-blocker-plugin (Geo/IP-based request blocking), built via xcaddy local replace. A daily sync workflow keeps it up to date.
Last upstream sync: 2026-03-26 — applied package updates from upstream 937e70d; merged upstream/develop (ours strategy) to resolve rewritten history divergence and skip 0acb430 (readme ports change not applicable to fork).
| Change | Description |
|---|---|
| Composeless l4-port-manager | The l4-port-manager sidecar can recreate the Caddy container using the Docker Engine API directly, without a bind-mounted docker-compose.yml |
| Pre-built Docker images | Multi-arch (amd64/arm64) images published to GHCR with separate build workflows for web, Caddy, and l4-port-manager |
| Macvlan mode | Zero-downtime L4 port changes — Caddy gets its own LAN IP via macvlan, L4 port changes become instant config reloads instead of container recreations |
| L4 feature parity | Full mTLS and Upstream TLS dial support for L4 proxy hosts — matching HTTP proxy capabilities with shared components and server-side certificate validation |
| WAF event management | Rule Library with built-in presets (WordPress, Django, Node.js) and custom rule sets; per-rule mute from the event drawer; real-time SecLang validation; data retention controls for analytics and WAF events |
| ACME certificate cache | TLS probe-based certificate metadata (issuer, validity, SANs) stored in a DB cache — instant cert page loads with no filesystem scanning |
| UI polish & consistency | Optimistic toggles, submit spinners, deferred config reloads; toggle-panel pattern (no dual chevron+toggle); aligned badge variants, filter icons, and mobile cards between HTTP and L4; dynamic TLS warning text |
| Interactive Storybook | Component and page interaction tests via @storybook/addon-vitest; deployed to GitHub Pages on every successful CI run |
Use the fork's pre-built GHCR images — no local build needed. Create a .env file with the required variables, then:
Caution
This fork ships bleeding-edge builds — images are rebuilt from the latest develop commit on every push. There are no stable or versioned tags. Pin to a specific image SHA (e.g. ghcr.io/zsdonny/caddy-proxy-manager-plus-web:sha-abc1234) if you need a reproducible deployment, and back up your data volume before pulling updated images.
docker compose up -dTip
No docker-compose.yml bind-mount is required. The l4-port-manager sidecar uses direct mode (Docker Engine API) and handles L4 port changes automatically without it.
Full docker-compose.yml (fork images, composeless direct mode)
services:
web:
container_name: caddy-proxy-manager-web
image: ghcr.io/zsdonny/caddy-proxy-manager-plus-web:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
SESSION_SECRET: ${SESSION_SECRET:?ERROR - SESSION_SECRET is required}
CADDY_API_URL: ${CADDY_API_URL:-http://caddy:2019}
CADDY_NETWORK_MODE: ${CADDY_NETWORK_MODE:-bridge}
BASE_URL: ${BASE_URL:-http://localhost:3000}
DATABASE_PATH: /app/data/caddy-proxy-manager.db
DATABASE_URL: file:/app/data/caddy-proxy-manager.db
NEXTAUTH_URL: ${BASE_URL:-http://localhost:3000}
ADMIN_USERNAME: ${ADMIN_USERNAME:?ERROR - ADMIN_USERNAME is required}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ERROR - ADMIN_PASSWORD is required}
group_add:
- "${CADDY_GID:-10000}"
volumes:
- caddy-manager-data:/app/data
- geoip-data:/usr/share/GeoIP:ro,z
- caddy-logs:/logs:ro
- caddy-data:/caddy-data:ro
depends_on:
caddy:
condition: service_healthy
networks:
- caddy-network
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health',r=>{process.exit(r.statusCode<400?0:1)}).on('error',()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 3
start_period: 60s
caddy:
container_name: caddy-proxy-manager-caddy
image: ghcr.io/zsdonny/caddy-proxy-manager-plus-caddy:latest
restart: unless-stopped
ports:
- "80:80"
- "80:80/udp"
- "443:443"
- "443:443/udp"
# Expose L4 (TCP/UDP) ports as needed, e.g.:
# - "5432:5432" # PostgreSQL
# - "6379:6379" # Redis
environment:
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-caddyproxymanager.com}
volumes:
- caddy-data:/data
- caddy-config:/config
- caddy-logs:/logs
- geoip-data:/usr/share/GeoIP:ro,z
networks:
- caddy-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "-O", "/dev/null", "http://localhost:2019/config/"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
l4-port-manager:
container_name: caddy-proxy-manager-l4-ports
image: ghcr.io/zsdonny/caddy-proxy-manager-plus-l4-port-manager:latest
restart: unless-stopped
environment:
DATA_DIR: /data
POLL_INTERVAL: "${L4_PORT_MANAGER_POLL_INTERVAL:-2}"
# COMPOSE_DIR is NOT mounted — activates composeless direct mode automatically
volumes:
# Must NOT be :ro — direct mode uses the Docker Engine API to recreate the Caddy container
- /var/run/docker.sock:/var/run/docker.sock
- caddy-manager-data:/data
depends_on:
caddy:
condition: service_healthy
# Optional: auto-update MaxMind GeoIP databases (requires a free MaxMind account)
geoipupdate:
container_name: geoipupdate
image: ghcr.io/maxmind/geoipupdate
profiles: [geoipupdate]
restart: always
environment:
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID:-}
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY:-}
GEOIPUPDATE_EDITION_IDS: "GeoLite2-ASN GeoLite2-City GeoLite2-Country"
GEOIPUPDATE_FREQUENCY: 72
volumes:
- geoip-data:/usr/share/GeoIP:z
networks:
- caddy-network
networks:
caddy-network:
driver: bridge
volumes:
caddy-manager-data:
caddy-data:
caddy-config:
caddy-logs:
geoip-data:Required .env variables:
| Variable | Description |
|---|---|
SESSION_SECRET |
Cookie/session encryption key — generate with openssl rand -base64 32 |
ADMIN_USERNAME |
Initial admin login username |
ADMIN_PASSWORD |
Initial admin login password (12+ chars, upper/lower/number/special) |
Optional .env variables:
| Variable | Default | Description |
|---|---|---|
BASE_URL |
http://localhost:3000 |
Public URL of the web UI |
PRIMARY_DOMAIN |
caddyproxymanager.com |
Primary domain configured in Caddy |
CADDY_GID |
10000 |
GID of the Caddy process — used to grant the web container read access to Caddy logs |
L4_PORT_MANAGER_POLL_INTERVAL |
2 |
Seconds between L4 trigger file checks |
GEOIPUPDATE_ACCOUNT_ID / GEOIPUPDATE_LICENSE_KEY |
— | MaxMind credentials for GeoIP auto-updates (enable with --profile geoipupdate) |
The upstream l4-port-manager requires your docker-compose.yml to be bind-mounted so it can run docker compose up when L4 port bindings change. This is not always practical when using pre-built images from a registry.
This fork adds a direct mode that falls back to the Docker Engine API when no compose file is present. Detection is automatic:
- Compose mode — used when
$COMPOSE_DIR/docker-compose.ymlexists (default:/compose/docker-compose.yml) - Direct mode — used otherwise; captures Caddy's full container config at startup and recreates it via
curlto the Docker socket on each L4 trigger
Note
On first startup in direct mode, the sidecar saves the Caddy container's configuration (image, networks, volumes, ports, environment) to $DATA_DIR/.l4-caddy-base-config.json. This file is recaptured automatically if the Caddy image changes. Delete it manually to force a recapture.
| Variable | Default | Description |
|---|---|---|
DATA_DIR |
/data |
Shared data volume (must match the web container) |
CADDY_CONTAINER_NAME |
caddy-proxy-manager-caddy |
Name of the Caddy container to recreate |
POLL_INTERVAL |
2 |
Seconds between trigger file checks |
COMPOSE_DIR |
/compose |
Path checked for docker-compose.yml (compose mode only) |
COMPOSE_PROJECT_NAME |
(auto) | Override compose project name (compose mode only) |
COMPOSE_HOST_DIR |
— | Host path of the project directory for compose bind-mount resolution |
DOCKER_SOCKET |
/var/run/docker.sock |
Docker socket path |
DOCKER_API_VERSION |
v1.43 |
Docker Engine API version. The sidecar auto-negotiates downward if the daemon's max version is lower — override only if auto-detection fails. |
Warning
Mount the Docker socket without :ro. Both compose mode and direct mode issue Docker API calls (docker inspect, docker compose, and raw Engine API requests) that require write access to the socket. A read-only mount causes all of these calls to fail and the sidecar will not function.
See the Quick Start section above for a complete docker-compose.yml using fork images in composeless direct mode.
In the default bridge mode, adding or removing L4 proxy host ports requires the caddy container to be recreated (Docker port bindings are immutable). With macvlan mode, Caddy gets its own static LAN IP and all ports bind directly — so L4 port changes become instant Caddy config reloads with no container restart.
Requirements: Linux host with a NIC that supports promiscuous mode. Test with:
ip link set <NIC> promisc on && ip link add test0 link <NIC> type macvlan mode bridge && ip link delete test0Portainer: Paste docker-compose.macvlan.yml into the stack editor (instead of docker-compose.yml) and set the required environment variables.
CLI: docker compose -f docker-compose.macvlan.yml up -d
| Variable | Example | Description |
|---|---|---|
MACVLAN_PARENT |
ens3 |
Host NIC (find with ip -brief link show) |
MACVLAN_SUBNET |
192.168.1.0/24 |
Your LAN subnet |
MACVLAN_GATEWAY |
192.168.1.1 |
Your LAN gateway |
MACVLAN_IP_RANGE |
192.168.1.200/30 |
IP range to assign Caddy from |
CADDY_MACVLAN_IP |
192.168.1.200 |
Static IP for Caddy on your LAN |
Note
The Docker host cannot reach Caddy's macvlan IP directly (Linux macvlan limitation). Other containers and LAN devices can. If host access is needed, create a macvlan shim interface on the host.
Note
The l4-port-manager sidecar is not included in docker-compose.macvlan.yml. It is not needed — L4 port changes apply instantly.
Use docker-compose.yml (or paste it into Portainer). The l4-port-manager sidecar will start and the "Apply Ports" banner will reappear when L4 port bindings need updating.
- Country flag emoji graphics are provided by Twemoji (maintained by @jdecked), licensed under CC BY 4.0.
Original Upstream README (fuomag9/caddy-proxy-manager)
Web interface for managing Caddy Server reverse proxies and certificates.
This project provides a web UI for Caddy Server, eliminating the need to manually edit JSON configurations or Caddyfiles. It handles reverse proxies, access lists, and certificate management through a shadcn/ui interface. Built with Next.js 16, React 19, shadcn/ui, Tailwind CSS, Drizzle ORM, and TypeScript.
git clone https://github.com/fuomag9/caddy-proxy-manager.git
cd caddy-proxy-manager
cp .env.example .env
# Edit .env with your credentials
docker compose up -dAccess at http://localhost:3000/login
Data persists in Docker volumes (caddy-manager-data, caddy-data, caddy-config, caddy-logs).
- Proxy Hosts - Reverse proxies with custom headers, multiple upstreams, load balancing, and enable/disable toggle
- L4 Proxy Hosts - TCP/UDP stream proxying with TLS SNI matching, proxy protocol (v1/v2), load balancing, health checks, and per-host geo blocking
- WAF - Web Application Firewall powered by Coraza with optional OWASP Core Rule Set (SQLi, XSS, LFI, RCE). Per-host enable/disable, global and per-host rule suppression, custom SecLang directives, and a searchable event log with severity and blocked/detected classification
- Analytics - Live traffic charts, protocol breakdown, country map, top user agents, and blocked request log with configurable time ranges
- Search & Pagination - Server-side search and pagination on all data tables (proxy hosts, access lists, audit log, certificates)
- Geo Blocking - Block or allow traffic by country, continent, ASN, CIDR range, or exact IP per proxy host
- Access Lists - Multi-account HTTP basic auth protection assignable per proxy host
- Certificates - Automatic HTTPS for every proxy host via Caddy ACME (Let's Encrypt / ZeroSSL), with issuer and expiry visibility + manual SSL/TLS import. Built-in CA for issuing internal client certificates
- Instance Sync - Master/slave configuration sync for multi-instance deployments. The master pushes proxy hosts, certificates, access lists, and settings to slaves on every change
- Settings - ACME email, Cloudflare DNS-01, upstream DNS pinning defaults, Authentik outpost, Prometheus metrics
- Audit Log - Searchable configuration change history with user attribution
- Mobile UI - Fully responsive interface optimised for iPhone and other narrow viewports
| Variable | Description | Default | Required |
|---|---|---|---|
SESSION_SECRET |
Session encryption key (32+ chars) | None | Yes |
ADMIN_USERNAME |
Admin login username | admin |
Yes |
ADMIN_PASSWORD |
Admin password (see requirements below) | admin (dev only) |
Yes |
BASE_URL |
Public URL where users access the dashboard. Required for OAuth - must match redirect URI |
http://localhost:3000 |
Yes (if using OAuth) |
CADDY_API_URL |
Caddy Admin API endpoint | http://caddy:2019 (prod)http://localhost:2019 (dev) |
No |
DATABASE_URL |
SQLite database URL | file:/app/data/caddy-proxy-manager.db |
No |
CERTS_DIRECTORY |
Certificate storage directory | ./data/certs |
No |
CADDY_CERTS_DIR |
Caddy cert storage path used for ACME metadata scanning (non-default deployments) | /caddy-data/caddy/certificates |
No |
LOGIN_MAX_ATTEMPTS |
Max login attempts before rate limit | 5 |
No |
LOGIN_WINDOW_MS |
Rate limit window in milliseconds | 300000 (5 min) |
No |
LOGIN_BLOCK_MS |
Rate limit block duration in milliseconds | 900000 (15 min) |
No |
OAUTH_ENABLED |
Enable OAuth2/OIDC authentication | false |
No |
OAUTH_PROVIDER_NAME |
Display name for OAuth provider | OAuth2 |
No |
OAUTH_CLIENT_ID |
OAuth2 client ID | None | No |
OAUTH_CLIENT_SECRET |
OAuth2 client secret | None | No |
OAUTH_ISSUER |
OAuth2 OIDC issuer URL | None | No |
OAUTH_AUTHORIZATION_URL |
Optional OAuth authorization endpoint override | Auto-discovered from OAUTH_ISSUER |
No |
OAUTH_TOKEN_URL |
Optional OAuth token endpoint override | Auto-discovered from OAUTH_ISSUER |
No |
OAUTH_USERINFO_URL |
Optional OAuth userinfo endpoint override | Auto-discovered from OAUTH_ISSUER |
No |
OAUTH_ALLOW_AUTO_LINKING |
Allow auto-linking OAuth identities to existing users | false |
No |
INSTANCE_MODE |
Instance role: standalone, master, or slave |
standalone |
No |
INSTANCE_SYNC_TOKEN |
Bearer token slaves use to authenticate sync requests | None | No (required if slave) |
INSTANCE_SLAVES |
JSON array of slave instances for the master to push to | None | No |
INSTANCE_SYNC_INTERVAL |
Periodic sync interval in seconds (0 = disabled) |
0 |
No |
INSTANCE_SYNC_ALLOW_HTTP |
Allow sync over HTTP (for internal Docker networks) | false |
No |
Production Requirements:
SESSION_SECRET: 32+ characters (openssl rand -base64 32)ADMIN_PASSWORD: 12+ chars with uppercase, lowercase, numbers, and special characters
Development mode (NODE_ENV=development) allows default admin/admin credentials.
- Production enforces strong passwords (12+ chars, mixed case, numbers, special characters)
- 32+ character session secrets required
- Login rate limiting: 5 attempts per 5 minutes
- Audit trail for all configuration changes
- Supports OAuth2/OIDC for SSO
Production Setup:
export SESSION_SECRET=$(openssl rand -base64 32)
export ADMIN_USERNAME="admin"
export ADMIN_PASSWORD="YourStr0ng-P@ssw0rd123!"
docker compose up -dLimitations:
- Certificate private keys stored unencrypted in SQLite
- In-memory rate limiting (not suitable for multi-instance deployments)
Caddy automatically obtains Let's Encrypt certificates for all proxy hosts.
Cloudflare DNS-01 (optional): Configure in Settings with a Cloudflare API token (Zone.DNS:Edit permissions).
Custom Certificates (optional): Import your own certificates via the Certificates page. Private keys are stored unencrypted in SQLite.
Geo blocking is configured per proxy host. It requires MaxMind GeoLite2 databases (see GeoIP Setup).
| Type | Example | Description |
|---|---|---|
| Country | DE |
ISO 3166-1 alpha-2 country code |
| Continent | EU |
AF, AN, AS, EU, NA, OC, SA |
| ASN | 24940 |
Autonomous System Number |
| CIDR | 91.98.150.0/24 |
IP range in CIDR notation |
| IP | 91.98.150.103 |
Exact IP address |
Rules can be block or allow. Allow rules take precedence over block rules — you can block an entire continent and then allow specific IPs or ASNs through.
Geo blocking requires MaxMind GeoLite2 Country and/or ASN databases. Use the bundled geoipupdate service:
- Register for a free MaxMind account at maxmind.com
- Generate a license key with
GeoLite2-CountryandGeoLite2-ASNpermissions - Add to your
.env:GEOIPUPDATE_ACCOUNT_ID=your-account-id GEOIPUPDATE_LICENSE_KEY=your-license-key - Start with the
geoipupdateprofile:docker compose --profile geoipupdate up -d
The databases are stored in the geoip-data Docker volume and shared between the web and Caddy containers.
The WAF is powered by Coraza and integrates the OWASP Core Rule Set.
Enable globally in WAF → Settings, then optionally override per proxy host. Two modes:
- Block — requests matching rules are rejected with 403
- Detect — requests are logged but not blocked
OWASP CRS covers SQLi, XSS, LFI, RCE, and more (enabled by default when WAF is on).
Rule suppression — suppress noisy rules globally or per host from the event detail drawer or the Suppressed Rules tab.
Custom directives — any ModSecurity SecLang syntax is accepted, e.g.:
SecRule REQUEST_URI "@beginsWith /api/" "id:9001,phase:1,ctl:ruleEngine=Off,nolog"
Run a master instance that pushes configuration to one or more slaves on every change.
# Master
INSTANCE_MODE=master
INSTANCE_SLAVES='[{"name":"replica","url":"https://replica.example.com","token":"<32-char-token>"}]'
# Slave
INSTANCE_MODE=slave
INSTANCE_SYNC_TOKEN=<32-char-token>Synced data: proxy hosts, certificates, access lists, and settings. User accounts are not synced.
Use HTTPS slave URLs in production. Set INSTANCE_SYNC_ALLOW_HTTP=true only for internal Docker networks.
See the Environment Variables Reference for all INSTANCE_* options.
You can enable upstream DNS pinning globally (Settings → Upstream DNS Pinning) and override per host (Proxy Host → Upstream DNS Pinning).
When enabled, hostname upstreams are resolved during config save/reload and written to Caddy as concrete IP dials. Address family selection supports:
both(preferred, resolves AAAA then A with IPv6 preference)ipv6ipv4
If one reverse proxy handler contains multiple different HTTPS upstream hostnames, HTTPS pinning is skipped for those HTTPS upstreams to avoid TLS SNI mismatch. In that case, hostname dials are kept for those HTTPS upstreams.
HTTP upstreams in the same handler are still eligible for pinning.
Supports any OIDC-compliant provider (Authentik, Keycloak, Auth0, etc.).
# Set your public URL (REQUIRED for OAuth to work)
BASE_URL=https://caddy-manager.example.com
OAUTH_ENABLED=true
OAUTH_PROVIDER_NAME="Authentik" # Display name
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_ISSUER=https://auth.example.com/application/o/app/Redirect URI Configuration:
You must configure this redirect URI in your OAuth provider:
{BASE_URL}/api/auth/callback/oauth2
Examples:
http://localhost:3000/api/auth/callback/oauth2(development)https://caddy-manager.example.com/api/auth/callback/oauth2(production)
The BASE_URL environment variable must match exactly where users access your dashboard.
OAuth login appears on the login page alongside credentials. Users can link OAuth to existing accounts from the Profile page.
- Multi-user RBAC
- Additional DNS providers (Route53, Namecheap, etc.)
Open an issue for feature requests.
Contributions welcome:
- Fork the repository
- Create a feature branch (
git checkout -b feature/name) - Commit changes (
git commit -m 'Add feature') - Push to branch (
git push origin feature/name) - Open a Pull Request
- Follow the existing code style (TypeScript, Prettier formatting)
- Add tests for new features when applicable
- Update documentation for user-facing changes
- Keep commits focused and write clear commit messages
- Issues: GitHub Issues for bugs and feature requests
- Discussions: GitHub Discussions for questions and ideas
This project is licensed under the MIT License - see the LICENSE file for details.
- Caddy Server – The amazing web server that powers this project
- Nginx Proxy Manager – The original project
- Next.js – React framework for production
- shadcn/ui – Beautifully designed components built on Radix UI and Tailwind CSS
- Drizzle ORM – Lightweight SQL migrations and type-safe queries