Skip to content

Install

One compose file and two generated secrets. Everything else is entered in the browser.

  1. Download caddy-proxy-manager-<version>-deploy.tar.gz from the latest release and unpack it into a directory of its own:

    Terminal window
    VERSION=v3.6.1 # the release you downloaded
    mkdir caddy-proxy-manager && cd caddy-proxy-manager
    tar -xzf ~/Downloads/caddy-proxy-manager-$VERSION-deploy.tar.gz

    The archive is flat - docker-compose.yml, .env.example and the few files they mount, all at the top level - so it runs from where it lands. Its compose file pins the images to that release, so what you deploy stays on it until you choose to move. No clone of the repository is needed. The managed CrowdSec container is included too, but stays off until you turn it on.

  2. Generate the two values Compose has no default for, straight into .env:

    Terminal window
    echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
    echo "POSTGRES_PASSWORD=$(openssl rand -base64 32)" >> .env
    chmod 600 .env

    That is the whole file. Compose declares those two ${SESSION_SECRET:?} and ${POSTGRES_PASSWORD:?} and refuses to start rather than come up on something guessable, so they are generated rather than chosen and nothing you would have had to think of ends up in the file.

    SESSION_SECRET is also the key every secret in the database is encrypted under: DNS provider credentials, imported private keys, agent secrets. It cannot live in the thing it encrypts, so generate it once and keep it. To rotate it, put the old value in SESSION_SECRET_PREVIOUS for one restart, which re-encrypts everything under the new one; changing it without that makes them unreadable. A backup is sealed with a passphrase instead, so it restores onto an instance with a different one.

  3. Start it:

    Terminal window
    docker compose up -d

    That also starts the postgres service the app keeps its data in, so there is no database server to install or run yourself.

    Caddy itself does not start yet - it sits behind a compose profile, and the agent starts it once the two are paired. An unpaired host answering 80 and 443 with a default page would be worse than one not listening at all.

  4. Open http://localhost:3000 and follow first run. Every URL redirects there until setup is finished; there is no administrator to sign in as until you create one.

Everything else is a field on the first run setup page, and afterwards on Settings: the base URL, OAuth, analytics, GeoIP, the dashboard host. Those values are stored in the database rather than in a file, encrypted at rest where they are secrets, and they win over anything .env says.

Prefer that over writing variables by hand, for reasons the file cannot match. Setup makes you sign in before the rest of the configuration is entered, so a mistyped password is found immediately rather than at the end. It shows the exact OAuth callback URL to register, which is derived and not guessable. And a credential saved there is handed to docker compose in the environment of the command the agent runs, instead of sitting in plaintext on the host for as long as the file exists.

.env.example is the reference for everything else - every variable that exists, what it defaults to, and whether Settings owns it. Read it when you need something setup does not cover: a PostgreSQL server other than the bundled one, a larger connection pool, a private CA for the agent, a deployment with no agent at all. Append the one line you need; there is no reason to carry the rest.

Port What
3000 The dashboard and API. Published so you always have a way in
80 / 443 Caddy, once it is running
2019 Caddy’s admin API. Bound only on the internal caddy-admin network, which web and the agent share and nothing can publish

Unpack the new release’s archive over the old one, in the same directory, then restart. .env is not in the archive, so it is left alone; the compose file is replaced with one naming the new images.

Terminal window
VERSION=v3.6.1 # the release you are moving to
tar -xzf ~/Downloads/caddy-proxy-manager-$VERSION-deploy.tar.gz
docker compose pull
docker compose up -d

Settings reports when a newer release is published to the registry you pull from. It can be switched off; the only other requests the app makes to the internet on its own are the CRS plugin registry check and the GeoIP downloads, each with a switch of its own.

The bundled database moved from PostgreSQL 17 to 18 during the 3.0 release candidates. PostgreSQL does not read another major version’s data files, so this one upgrade is a dump and restore rather than a pull. It applies if your docker-compose.yml still says image: postgres:17-alpine.

Nothing is lost by getting the order wrong. The 18 service refuses to start on a volume that still holds a 17 cluster - it restarts with an error naming the version, and the data stays where it was. If you already updated the compose file, put the two old lines back for the first step: image: postgres:17-alpine, and the volume mounted at /var/lib/postgresql/data.

  1. With the old compose file, stop the app and dump its database from the 17 server:

    Terminal window
    docker compose stop web
    docker compose exec postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom --file=/tmp/cpm.dump'
    docker compose cp postgres:/tmp/cpm.dump ./cpm.dump

    The dump is copied out rather than redirected to a file, so it arrives intact from any shell - PowerShell’s > re-encodes what passes through it. Keep cpm.dump until you have signed in afterwards.

  2. Unpack the new release’s archive over the old files, then remove the containers and the 17 volume:

    Terminal window
    docker compose down
    docker volume rm caddy-proxy-manager_postgres-data

    down without -v leaves every other volume alone, certificates included. The database volume is named after the Compose project, which is the directory the compose file is in unless COMPOSE_PROJECT_NAME says otherwise - docker volume ls shows it.

  3. Start PostgreSQL 18, restore into it, and bring the rest back:

    Terminal window
    docker compose up -d --wait postgres
    docker compose cp ./cpm.dump postgres:/tmp/cpm.dump
    docker compose exec postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --exit-on-error /tmp/cpm.dump && rm /tmp/cpm.dump'
    docker compose up -d

    Accounts, hosts, certificates and settings come back as they were, and you sign in with the same password. Migrations that already ran are recorded in the dump, so none of them run again.

A server you run yourself (POSTGRES_HOST) is unaffected: the app supports 17 and 18 alike, and when to upgrade it is yours to decide.

Before 3.0 the configuration lived in .env, and installing meant copying the whole reference over and editing it:

Terminal window
cp .env.example .env

It still works. Resolution order is stored value → .env → built-in default, so a variable set there is honoured and still beats the default - it is only overridden once the same setting is saved in the app, at which point the line can be deleted.

But it is no longer the shape of a new install. Most settings now live in the database, so a copied .env.example is mostly commented-out lines that duplicate what the setup page will ask you for anyway, and every secret written into it is a plaintext copy that the Settings path would not have made. Use it when you are provisioning unattended and there is no browser to reach the setup page in - setting both ADMIN_USERNAME and ADMIN_PASSWORD seeds an administrator at startup and skips the flow entirely.