Install
One compose file and two generated secrets. Everything else is entered in the browser.
Installation
Section titled “Installation”-
Download
caddy-proxy-manager-<version>-deploy.tar.gzfrom the latest release and unpack it into a directory of its own:Terminal window VERSION=v3.6.1 # the release you downloadedmkdir caddy-proxy-manager && cd caddy-proxy-managertar -xzf ~/Downloads/caddy-proxy-manager-$VERSION-deploy.tar.gzThe archive is flat -
docker-compose.yml,.env.exampleand 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. -
Generate the two values Compose has no default for, straight into
.env:Terminal window echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .envecho "POSTGRES_PASSWORD=$(openssl rand -base64 32)" >> .envchmod 600 .envThat 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_SECRETis 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 inSESSION_SECRET_PREVIOUSfor 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. -
Start it:
Terminal window docker compose up -dThat also starts the
postgresservice 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.
-
Open
http://localhost:3000and follow first run. Every URL redirects there until setup is finished; there is no administrator to sign in as until you create one.
Configure it in the browser
Section titled “Configure it in the browser”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 |
Upgrading
Section titled “Upgrading”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.
VERSION=v3.6.1 # the release you are moving totar -xzf ~/Downloads/caddy-proxy-manager-$VERSION-deploy.tar.gzdocker compose pulldocker compose up -dSettings 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.
From PostgreSQL 17
Section titled “From PostgreSQL 17”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.
-
With the old compose file, stop the app and dump its database from the 17 server:
Terminal window docker compose stop webdocker 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.dumpThe 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. Keepcpm.dumpuntil you have signed in afterwards. -
Unpack the new release’s archive over the old files, then remove the containers and the 17 volume:
Terminal window docker compose downdocker volume rm caddy-proxy-manager_postgres-datadownwithout-vleaves every other volume alone, certificates included. The database volume is named after the Compose project, which is the directory the compose file is in unlessCOMPOSE_PROJECT_NAMEsays otherwise -docker volume lsshows it. -
Start PostgreSQL 18, restore into it, and bring the rest back:
Terminal window docker compose up -d --wait postgresdocker compose cp ./cpm.dump postgres:/tmp/cpm.dumpdocker 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 -dAccounts, 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.
The legacy method
Section titled “The legacy method”Before 3.0 the configuration lived in .env, and installing meant copying the whole reference over
and editing it:
cp .env.example .envIt 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.