Skip to content

Installation

X4Applegate edited this page Sep 10, 2026 · 1 revision

Installation

CaddyUI is one container (or one binary) that talks to Caddy over the admin API. Caddy itself runs separately: in the same Compose stack, on the same host, or on remote "edge" nodes reached over a private network.

Docker Compose (recommended)

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddyui-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    environment:
      CADDY_ADMIN: 0.0.0.0:2019
    command: >-
      mkdir -p /config/caddy;
      [ -f /config/caddy/autosave.json ] || echo '{}' > /config/caddy/autosave.json;
      exec caddy run --config /config/caddy/autosave.json --resume --adapter json
    networks:
      - caddyui

  caddyui:
    image: applegater/caddyui:latest
    container_name: caddyui
    restart: unless-stopped
    depends_on:
      - caddy
    ports:
      - "8081:8080"
    volumes:
      - caddyui_data:/data
    environment:
      CADDYUI_DB: /data/caddyui.db
      CADDYUI_LISTEN: :8080
      CADDY_ADMIN_URL: http://caddy:2019
    networks:
      - caddyui

volumes:
  caddy_data:
  caddy_config:
  caddyui_data:

networks:
  caddyui:
    driver: bridge

Open http://localhost:8081 and create the first administrator account. See Configuration for what happens next.

Why the command block? Caddy normally loads only its Caddyfile at boot and forgets anything pushed to the admin API. --resume makes it boot from autosave.json, the last config it received, so CaddyUI's pushes survive a docker compose restart. On a fresh install that file does not exist yet and Caddy would crash-loop, so the command seeds an empty {} config the first time.

Port 2019 is not published in this stack. CaddyUI reaches it over the private Compose network as http://caddy:2019. Keep it that way: the admin API has no authentication and whoever reaches it controls Caddy. See Security and Users for the options when Caddy runs elsewhere.

The repository's own docker-compose.yml is the same stack with the custom Caddy build below, a TZ variable shared by both containers, a persisted /var/log/caddy volume for file access logs, and commented examples for mounting certificate directories and the Caddy data volume.

Custom Caddy build

The stock caddy:2-alpine image has no DNS provider modules, so it cannot solve ACME DNS-01 challenges (wildcard certificates, hosts that are not reachable on port 80) and has no CrowdSec bouncer. The repository ships Dockerfile.caddy, which builds Caddy with every DNS provider CaddyUI supports (Cloudflare, Porkbun, Namecheap, GoDaddy, DigitalOcean, Hetzner, Route 53, Gandi) and the CrowdSec HTTP bouncer.

  caddy:
    build:
      context: .
      dockerfile: Dockerfile.caddy
    image: caddyui-caddy:latest

Use it whenever you plan to use Managed DNS certificates, DNS Providers or the CrowdSec integration. Plain HTTP-01 proxy hosts work fine with the stock image.

Docker run

docker run -d \
  --name caddyui \
  -p 8081:8080 \
  -v caddyui_data:/data \
  -e CADDY_ADMIN_URL=http://your-caddy-host:2019 \
  applegater/caddyui:latest

Bind mounts: if you mount a host directory to /data instead of a named volume, it must be owned by uid 10001, the non-root user the container runs as. The container prints a clear error at start-up when the directory is not writable.

sudo chown 10001:10001 /path/to/caddyui_data

MariaDB backend

SQLite is the zero-configuration default and right for most installs. MariaDB is available for larger installations that need concurrent UI reads, continuous analytics ingestion, replication or platform-managed backups.

With the included overlay:

export CADDYUI_MARIADB_PASSWORD='replace-with-a-long-random-password'
export CADDYUI_MARIADB_ROOT_PASSWORD='replace-with-a-different-random-password'
docker compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d

Against an existing MariaDB server:

environment:
  CADDYUI_DB_DRIVER: mariadb
  CADDYUI_DB_DSN: caddyui:password@tcp(mariadb.example.internal:3306)/caddyui

Create the database and user first. CaddyUI creates and migrates its tables automatically and never writes the DSN to its logs.

Migrating an existing SQLite install

Take a timestamped copy of caddyui.db, stop the normal CaddyUI container, start the empty MariaDB service and run the migration command:

docker compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d mariadb
docker compose -f docker-compose.yml -f docker-compose.mariadb.yml run --rm --no-deps \
  caddyui migrate-db \
  --from-sqlite /data/caddyui.db
docker compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d

The SQLite file is opened read-only and the MariaDB destination must be empty. Add --skip-analytics to leave the large access_events and access_daily tables behind, which is strongly recommended when the SQLite file grew mostly because of visitor analytics.

MariaDB backups are your database platform's job (mariadb-dump, snapshots, point-in-time recovery). Caddy configuration snapshots inside CaddyUI work on both backends.

systemd binary install

Tagged GitHub releases include linux/amd64 and linux/arm64 tarballs with the caddyui binary, a sample caddyui.service unit and an installer. This suits LXC/Proxmox labs, VMs, or any host where you prefer no Docker.

  1. Install Caddy separately and make sure its admin API is reachable from the CaddyUI host. On the same host the default is http://127.0.0.1:2019.
  2. Download the archive for your architecture from the latest release.
  3. Extract and run the installer:
tar -xvf ./caddyui_vX.Y.Z_linux_ARCH.tar.gz
cd caddyui_vX.Y.Z_linux_ARCH
./install.sh

The packaged service stores SQLite data at /var/lib/caddyui/caddyui.db, listens on 127.0.0.1:8080, talks to Caddy at http://127.0.0.1:2019 and binds the log-ingest listener to 127.0.0.1:9019. Override with a drop-in:

sudo systemctl edit caddyui
[Service]
Environment=CADDYUI_LISTEN=0.0.0.0:8080
Environment=CADDY_ADMIN_URL=http://10.8.0.2:2019
# Optional: disable the ingest listener if you do not use analytics, certificate
# lifecycle status or Server Logs.
Environment=CADDYUI_INGEST_LISTEN=
sudo systemctl restart caddyui

Building the binary yourself is covered in Development and Releases.

Agent mode: edge-only Caddy nodes

For several hosts you need one CaddyUI. Every other host runs only Caddy, and the central CaddyUI manages it through the admin API, usually over WireGuard or Tailscale. No database, no UI container and no extra public port on the edge.

On each edge host:

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    # --resume is required so admin-API pushes persist across Caddy restarts.
    command: >-
      mkdir -p /config/caddy;
      [ -f /config/caddy/autosave.json ] || echo '{}' > /config/caddy/autosave.json;
      exec caddy run --config /config/caddy/autosave.json --resume --adapter json
    ports:
      # Bind the admin API to your private tunnel IP (WireGuard / Tailscale).
      # Do NOT expose :2019 on a public interface.
      - "10.8.0.2:2019:2019"
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Then in the central CaddyUI open Caddy Fleet → Add server and point it at http://10.8.0.2:2019. Give the node its own Log ingest target (the CaddyUI host as that node can reach it, for example 10.8.0.1:9019) so analytics, certificate status and Server Logs work for it too. See Caddy Fleet.

Image tags

Tag Points at
:vX.Y.Z A specific release, immutable. Recommended for production.
:stable The current release. Updated on every release.
:latest Kept in lockstep with :stable.

All three resolve to the same image on release day. Images are multi-arch (linux/amd64, linux/arm64), built from scratch, and run as non-root uid 10001. The old rolling :preview tag is retired and no longer updated.

Upgrading

  1. Pull the new tag.
  2. Recreate the container. Portainer: Recreate with Re-pull image enabled. CLI: docker compose pull && docker compose up -d.
  3. Database migrations run automatically at start-up; the log lists each one applied.
  4. Read the CHANGELOG entry for anything that needs attention.

Downgrading is not supported once a newer version has migrated the database. Take a backup first (see Snapshots and Backup).

Clone this wiki locally