A Next.js management UI for Caddy — edit your Caddyfile, validate, format, reload, and tail logs from a browser.
The quickest way to kick the tires before committing to a full install. Requires Docker with the Compose plugin.
git clone https://github.com/JohnnyRacket/Simple-Caddy-UI.git caddy-ui
cd caddy-ui
docker compose upOpen http://localhost:3000 for the UI and http://localhost:8080 for the Caddy web server. Runs in dev mode against a sandboxed Caddy instance — no changes to your host system.
The Docker environment uses SECRET_TOKEN=password by default. Visit http://localhost:3000/login and enter password to authenticate. To use a different token, set SECRET_TOKEN in a .env file at the project root before running docker compose up.
These instructions are for deploying on a Linux machine (e.g. Raspberry Pi) that is already running Caddy.
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify
node --version # should be v24.xRunning the app as a dedicated system user limits the blast radius if anything goes wrong.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin caddy-uisudo git clone https://github.com/JohnnyRacket/Simple-Caddy-UI.git /opt/caddy-ui
sudo chown -R caddy-ui:caddy-ui /opt/caddy-ui
cd /opt/caddy-ui
sudo -u caddy-ui npm install
# Create env file — adjust path if your Caddyfile is elsewhere
# Generate a strong token with: openssl rand -hex 32
sudo -u caddy-ui tee /opt/caddy-ui/.env.local << 'EOF'
CADDYFILE_PATH=/etc/caddy/Caddyfile
SECRET_TOKEN=your-secret-token-here
EOF
sudo -u caddy-ui npm run build
LOCAL_ONLY— by default the app rejects requests from non-LAN IPs. If you're running behind a reverse proxy or need to adjust this, addLOCAL_ONLY=falseto your.env.local.
Binary path overrides — the app defaults to standard Debian/Ubuntu paths (
/bin/cp,/usr/bin/caddy,/bin/systemctl,/usr/bin/journalctl). On other distros, override them in.env.local:CP_BIN=/usr/bin/cp CADDY_BIN=/usr/local/bin/caddy SYSTEMCTL_BIN=/usr/bin/systemctl JOURNALCTL_BIN=/usr/bin/journalctl
The app needs passwordless sudo for a few specific commands.
sudo visudo -f /etc/sudoers.d/caddy-uiPaste the following, then save and exit:
caddy-ui ALL=(ALL) NOPASSWD: /bin/cp /tmp/caddyfile-*/Caddyfile /etc/caddy/Caddyfile
caddy-ui ALL=(ALL) NOPASSWD: /bin/systemctl reload caddy
caddy-ui ALL=(ALL) NOPASSWD: /usr/bin/caddy validate --config /etc/caddy/Caddyfile
caddy-ui ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u caddy -f --no-pager -o short-iso
Verify it works:
sudo -u caddy-ui sudo /bin/systemctl reload caddyThis keeps the app running and restarts it on failure.
sudo nano /etc/systemd/system/caddy-ui.serviceNext.js automatically loads .env.local at startup, so no extra environment configuration is needed in the service file. To change the port, set PORT=<number> in .env.local — Next.js reads it natively.
[Unit]
Description=Caddy UI
After=network.target
[Service]
Type=simple
User=caddy-ui
WorkingDirectory=/opt/caddy-ui
ExecStart=/usr/bin/npm start
Restart=on-failure
Environment=NODE_ENV=production PORT=3000
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now caddy-ui
# Check it's running
sudo systemctl status caddy-uiOnce running, the UI is available at http://<machine-ip>:3000 from any device on your local network.
sudo -u caddy-ui git -C /opt/caddy-ui pull
sudo -u caddy-ui npm --prefix /opt/caddy-ui install
sudo -u caddy-ui npm --prefix /opt/caddy-ui run build
sudo systemctl restart caddy-uicaddy-ui is designed for LAN/trusted-network use only.
- Do not expose port 3000 to the public internet. The app has no brute-force protection beyond the built-in rate limiter, and a compromised token grants full Caddyfile write access.
- Auth is a single shared secret.
SECRET_TOKENis a bearer token, not a user account system. Treat it like a password and rotate it if exposed. - The app runs with limited
sudoaccess. Only the specific commands in the sudoers file are allowed — it cannot run arbitrary commands as root. - LAN enforcement is enabled by default, but is not a security boundary.
LOCAL_ONLY=trueblocks requests whose IP doesn't match a known private range, but this check runs in application middleware — it can be fooled by misconfigured proxies, spoofed headers, or future code changes. It is a convenience guard for accidental exposure, not a hardened firewall rule. Do not rely on it as your only protection. If you need to restrict access, do it at the network level (firewall, VPN, or reverse proxy with proper IP allowlisting).
The install paths and usernames used in this README (/opt/caddy-ui, caddy-ui user) are conventions, not requirements. Adapt them to your environment as needed.