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
Info:

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:

curl -fsSL https://github.com/proliferate-ai/proliferate/releases/download/server-vX.Y.Z/proliferate-deploy.tar.gz | tar xz && cd proliferate-deploy && cp .env.production.example .env.static

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:

# .env.static
SITE_ADDRESS=proliferate.company.com
PROLIFERATE_TELEMETRY_MODE=self_managed
PROLIFERATE_SERVER_IMAGE_TAG=X.Y.Z
 
# Leave blank: bootstrap generates and persists these
JWT_SECRET=
CLOUD_SECRET_KEY=
POSTGRES_PASSWORD=

Required:

VariableWhat it does
SITE_ADDRESSPublic 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_MODESet 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_TAGThe server image tag to run. Pin the release version you fetched the bundle for; stable follows the latest release.

Secrets, auto-generated when blank:

VariableWhat it does
JWT_SECRETSigns session tokens.
CLOUD_SECRET_KEYEncrypts stored credentials at rest and signs cloud workspace flows.
POSTGRES_PASSWORDPassword for the bundled Postgres.

Leave all three blank and bootstrap generates them on first run and persists them in .env.generated.

Common optional settings:

VariableWhat it does
API_BASE_URLThe 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_EMAILSComma-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_DOMAINSComma-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_SECRETOptional GitHub sign-in for desktops, callback https://<site>/auth/desktop/github/callback. When unset, desktops sign in with email and password.
SINGLE_ORG_MODEWhether 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_DISABLEDSet to 1 to turn off the anonymous first-party telemetry too. See Telemetry & privacy.
RESEND_API_KEY / RESEND_FROM_EMAILOptional Resend credentials for invitation email delivery. Invitations work without any email provider; you share the registration link yourself.
PROLIFERATE_SERVER_IMAGEAlternate image repository, for example a private ECR mirror (set AWS_REGION too in that case).
POSTGRES_DB / POSTGRES_USERBundled Postgres database name and user, default proliferate.
PROLIFERATE_PUBLIC_HEALTHCHECK_URLPublic URL bootstrap checks after the local health check passes. Defaults to https://<SITE_ADDRESS>/health.

The full reference is at Environment variables.

Bootstrap

./bootstrap.sh

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:

First-run setup is pending. Claim this instance in a browser:

  Setup token: 7f3a...c91b
  Claim URL:   https://proliferate.company.com/setup

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:

curl -fsS https://proliferate.company.com/health
curl -fsS https://proliferate.company.com/meta

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.

Info:

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:

  1. ensure-secrets.sh merges .env.static with optional .env.local overrides, generates any blank secrets, derives API_BASE_URL and the public health check URL from SITE_ADDRESS when 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).
  2. registry-login.sh is a no-op for ghcr.io; it logs Docker into a private ECR registry when PROLIFERATE_SERVER_IMAGE points at one.
  3. install-runtime.sh downloads and checksum-verifies the runtime binaries into /opt/proliferate/bin when the CLOUD_*_SOURCE_BINARY_PATH variables are set. A base install skips this; it matters for cloud sandboxes.
  4. Compose: up -d db, then run --rm migrate, then up -d api caddy.
  5. wait-for-health.sh polls http://127.0.0.1:8000/health, then the public https://<SITE_ADDRESS>/health, then prints the setup token and claim URL while the instance is unclaimed.

The env files, at a glance:

FileWho writes itNotes
.env.staticYouYour configuration. The only file you edit routinely.
.env.localYou (optional)Per-host overrides; values here win over .env.static.
.env.generatedBootstrapGenerated secrets. Preserve and back up this file; losing it means losing the encryption and signing keys.
.env.runtimeBootstrapMerged output Compose reads. Regenerated every run; never edit it.

Local evaluation

Info:

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.

Warning:

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:

{$SITE_ADDRESS} {
  encode zstd gzip
  reverse_proxy api:8000
}

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:

{$SITE_ADDRESS} {
  encode zstd gzip
  tls /certs/server.crt /certs/server.key
  reverse_proxy api:8000
}

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

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

On this page