Docker on any server
The canonical self-hosted deployment: Docker Compose on a Linux host.
Every self-hosted Proliferate runs the same Docker Compose stack, shipped as a deploy bundle with each server release. The cloud guides (AWS, GCP, Azure) are just ways of getting a host that runs this page. Everything here applies to all of them.
What you get
Four Compose services:
- caddy terminates HTTPS with automatic certificates for your domain
- db is Postgres 16, the only stateful service
- migrate is a one-shot container that runs database migrations
- api is the Proliferate server, from
ghcr.io/proliferate-ai/proliferate-server
Two honest limits of self-hosting today: automations and background jobs are not available (the stack ships no worker tier yet), and the web app is not self-hostable in v1. Your team connects with the desktop app.
Deploy
Prerequisites
A Linux host with Docker and Compose v2, a DNS name pointed at it, and ports 80/443 open. The server is light. See Sizing & scaling for numbers.
Fetch the deploy bundle
Each server release publishes the deploy files as a tarball. Fetch it, unpack it, and copy the env template:
Replace X.Y.Z with the release you are installing. No repository clone is
needed.
Configure
Edit .env.static. This is the minimal truthful set for a base install:
Required:
| Variable | What it does |
|---|---|
SITE_ADDRESS | Public hostname Caddy serves (for example proliferate.company.com), with or without a scheme (https is assumed when you leave it off). Point DNS at the host before first boot so a certificate can be issued. |
PROLIFERATE_TELEMETRY_MODE | Set to self_managed on every self-hosted install. Limits reporting to anonymous first-party telemetry and turns on self-host defaults such as single-org mode. |
PROLIFERATE_SERVER_IMAGE_TAG | The server image tag to run. Pin the release version you fetched the bundle for; stable follows the latest release. |
Secrets, auto-generated when blank:
| Variable | What it does |
|---|---|
JWT_SECRET | Signs session tokens. |
CLOUD_SECRET_KEY | Encrypts stored credentials at rest and signs cloud workspace flows. |
POSTGRES_PASSWORD | Password for the bundled Postgres. |
Leave all three blank and bootstrap generates them on first run and persists
them in .env.generated.
Common optional settings:
| Variable | What it does |
|---|---|
API_BASE_URL | The full public URL of the Proliferate server. Bootstrap derives it from SITE_ADDRESS when unset, which is right for almost everyone; an explicit value wins. The server bakes this URL into configuration it pushes to desktops and runtimes. |
ADMIN_EMAILS | Comma-separated emails that always hold at least the admin role. Asserted at every sign-in, so adding an email and restarting is the lockout-recovery path. Removal from the list never demotes anyone, and the last admin is protected. Listed emails are reinstated as admin at their next sign-in even after being removed from the organization, so offboarding someone on the list means removing them here too. |
ALLOWED_EMAIL_DOMAINS | Comma-separated email domains invited users must belong to before they can self-register. A gate on top of invitations, never a grant. |
GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET | Optional GitHub sign-in for desktops, callback https://<site>/auth/desktop/github/callback. When unset, desktops sign in with email and password. |
SINGLE_ORG_MODE | Whether every user joins the one instance organization. Defaults to true whenever PROLIFERATE_TELEMETRY_MODE is not hosted_product, so self-hosted installs get it with no configuration. |
PROLIFERATE_ANONYMOUS_TELEMETRY_DISABLED | Set to 1 to turn off the anonymous first-party telemetry too. See Telemetry & privacy. |
RESEND_API_KEY / RESEND_FROM_EMAIL | Optional Resend credentials for invitation email delivery. Invitations work without any email provider; you share the registration link yourself. |
PROLIFERATE_SERVER_IMAGE | Alternate image repository, for example a private ECR mirror (set AWS_REGION too in that case). |
POSTGRES_DB / POSTGRES_USER | Bundled Postgres database name and user, default proliferate. |
PROLIFERATE_PUBLIC_HEALTHCHECK_URL | Public URL bootstrap checks after the local health check passes. Defaults to https://<SITE_ADDRESS>/health. |
The full reference is at Environment variables.
Bootstrap
Bootstrap generates any missing secrets, starts Postgres, runs migrations, brings the API up behind Caddy with automatic HTTPS, waits for the local and public health checks, and then prints a one-time setup token and claim URL:
Terminal capture: ./bootstrap.sh output ending with the setup token and claim URL
The tail of a bootstrap run: compose services starting, health checks passing, then the setup token block.
The token stays available at /var/lib/proliferate/setup/setup-token inside
the api container until the instance is claimed. If you lose the terminal
output, rerun ./wait-for-health.sh to print it again.
Claim your instance
Open https://<your-site>/setup in a browser and enter an email, a password,
an organization name if you want one (blank derives a name from your email
domain; the derived default shows as the placeholder), and the setup token.
That account becomes the owner of the instance organization (self-hosted
servers run in single-org mode: everyone shares one organization). The
/setup page returns 404 forever after the claim.
Screenshot: the /setup claim page with email, password, optional organization name, and setup token fields
The server-rendered first-run claim form in a browser, with the derived organization name shown as the placeholder.
Verify
Confirm the public endpoints:
GET /meta returns serverVersion, desktopVersion, runtimeVersion, and
minDesktopVersion, the versions your server pins for the whole fleet.
GET /desktop/updater/latest.json should respond with a 302 redirect to the
official CDN manifest for the pinned desktop version (or to the latest stable
manifest when that version's manifest is not published yet, so update checks
always land somewhere valid).
Then connect a desktop app
(Connect the desktop app): write
~/.proliferate/config.json with
{"apiBaseUrl": "https://proliferate.company.com"} and restart the app (the
file is read once at startup). The sign-in screen probes
GET /auth/desktop/methods and shows an email and password form by default;
the GitHub button appears only when GitHub OAuth is configured.
In-product server connect and the proliferate://connect deep link are
planned. Today the config file is the way to point a desktop at your server.
What bootstrap actually does
bootstrap.sh is a thin orchestrator over four scripts in the bundle:
ensure-secrets.shmerges.env.staticwith optional.env.localoverrides, generates any blank secrets, derivesAPI_BASE_URLand the public health check URL fromSITE_ADDRESSwhen you have not set them (explicit values win), and writes two files:.env.generated(the secrets of record, mode 600) and.env.runtime(the merged file Compose actually reads, regenerated on every run).registry-login.shis a no-op forghcr.io; it logs Docker into a private ECR registry whenPROLIFERATE_SERVER_IMAGEpoints at one.install-runtime.shdownloads and checksum-verifies the runtime binaries into/opt/proliferate/binwhen theCLOUD_*_SOURCE_BINARY_PATHvariables are set. A base install skips this; it matters for cloud sandboxes.- Compose:
up -d db, thenrun --rm migrate, thenup -d api caddy. wait-for-health.shpollshttp://127.0.0.1:8000/health, then the publichttps://<SITE_ADDRESS>/health, then prints the setup token and claim URL while the instance is unclaimed.
The env files, at a glance:
| File | Who writes it | Notes |
|---|---|---|
.env.static | You | Your configuration. The only file you edit routinely. |
.env.local | You (optional) | Per-host overrides; values here win over .env.static. |
.env.generated | Bootstrap | Generated secrets. Preserve and back up this file; losing it means losing the encryption and signing keys. |
.env.runtime | Bootstrap | Merged output Compose reads. Regenerated every run; never edit it. |
Local evaluation
You can run the stack on localhost without a public domain to kick the
tires: set SITE_ADDRESS=localhost and Caddy issues a certificate from its
internal CA instead of a public one. Everything works except cloud sandboxes.
This mode is for evaluation only.
Cloud sandboxes require a publicly reachable HTTPS URL. Sandboxes call back
into your server, so a localhost install cannot use that add-on.
Bring your own certificate
The shipped Caddyfile is four lines:
By default Caddy provisions publicly trusted certificates automatically, which requires public DNS and reachable ports 80/443. If your host cannot do that (internal network, corporate CA), point Caddy at your own certificate:
Mount the certificate directory into the caddy service in
docker-compose.production.yml (for example ./certs:/certs:ro). If the
certificate comes from an internal CA, every machine running the desktop app
must trust that CA.
Updating
update.sh pulls the pinned image, reruns migrations, and restarts the stack
(pull, run --rm migrate, up -d). To move to a new release, set
PROLIFERATE_SERVER_IMAGE_TAG in .env.static and run it. Connected desktop
apps follow the version your server advertises, so updating the server updates
the fleet. See
Updates & versioning.
Next steps
- Invite your team: an admin invites an email and shares the invite link, and the invitee creates their account in a browser. No email provider required.
- Cloud sandboxes
- Model gateway
- Updates & versioning