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 in a base install:
- caddy terminates HTTPS with automatic certificates for your domain and
proxies everything to the
apiservice - 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. It answers the API and, by default, also serves the compiled Web app: the server image bakes in the built ProductClient Web distribution, andWEB_DIST_DIRin the shipped Compose file points at it, so one public URL serves both a browser and the desktop app. No separate Web container and no Node process run at runtime.
Two more optional service groups exist in the same Compose file, off by
default and turned on automatically by your configuration (see
Compose profiles for optional services
below): litellm + litellm-db for the
model gateway, and redis for the
cloud sandboxes add-on (coming
soon).
One honest limit of self-hosting today: automations and background jobs are not available, because the stack ships no worker tier yet.
Deploy
Prerequisites
A Linux host (x86_64 or aarch64) with Docker Engine and the Compose v2 plugin, a DNS name pointed at it, and ports 80/443 open. The server is light. See Sizing & scaling for numbers.
Run the installer
The guided installer resolves the newest published server-v* release (never
GitHub's generic "latest," which is usually a bundle-less tag), downloads and
checksum-verifies the deploy bundle, installs it to
/opt/proliferate/server/deploy, generates .env.static, runs the
configuration preflight, and boots the stack. Inspect it first, then run it as
root:
Or pipe straight to a shell:
Useful flags:
| Flag | Effect |
|---|---|
-d, --domain HOST | Public hostname; Caddy issues TLS for it. Point DNS at it first. |
--eval | No domain: derive an sslip.io hostname from the host's public IP, with real Let's Encrypt TLS. Mutually exclusive with --domain. |
-v, --version X.Y.Z | Pin a specific server release instead of the newest one. Also re-pins the image tag on a rerun. |
--install-root DIR | Durable install root, default /opt/proliferate. |
--no-start | Fetch and configure only; do not boot the stack. |
--dry-run | Print the resolved plan and exit; makes no changes. |
-y, --yes | Skip the confirmation prompt (used automatically when piped). |
The installer is idempotent: rerunning it refreshes the bundle's scripts
without ever overwriting your .env.static, generated secrets, or data. It
ends by printing the claim URL and the setup token needed to open it, plus the
management commands (update.sh, doctor.sh) at their installed path.
Prefer to manage the deploy bundle yourself instead of running the installer script — a custom orchestration layer, an air-gapped mirror, or just wanting to see every step? See Manual setup below; it produces the identical stack.
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.
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).
Open https://proliferate.company.com in a browser: it is the same address
you just claimed, serving the Web app from the same api service.
Connect a desktop app too if you or your teammates need local repositories or
a local AnyHarness runtime
(Connect the desktop app): choose
Connect to a server on the sign-in screen and enter
https://proliferate.company.com. The app verifies the server, saves the
address, and relaunches connected to it. 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. For fleet
provisioning, write ~/.proliferate/config.json directly instead.
Manual setup
Everything here is what install.sh automates. Use this path if you want to
manage the deploy directory yourself; both paths produce the identical
Compose stack, and both run the same preflight.sh validation before
touching containers.
Fetch the deploy bundle
Each server release publishes the deploy files as a tarball. Replace
X.Y.Z with the release you are installing (see the
releases page —
only tags prefixed server-v carry this asset):
Install this somewhere durable — /opt/proliferate is the convention the
installer uses and what the rest of these docs assume; avoid /tmp, which a
reboot wipes.
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 <API_BASE_URL>/auth/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, validates the resolved configuration
with preflight.sh (so a dangerous partial config fails here instead of
crash-looping a running instance), starts Postgres, runs migrations, brings the API up behind Caddy
with automatic HTTPS, brings up any optional services your config enables
(the bundled model gateway or the cloud-workspaces Redis lock — see
Compose profiles below), waits for
the local and public health checks, and then prints a one-time setup token and
claim URL:
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.
What bootstrap actually does
bootstrap.sh is a thin orchestrator over the 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).preflight.shvalidates the resolved.env.runtimeand refuses to proceed on a dangerous partial config (see the callout above), before anydocker composelifecycle command runs.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; the variables belong to the cloud sandboxes add-on (coming soon).- Compose:
up -d db, thenrun --rm migrate, thenup -d api caddy. - Compose profiles: brings up and waits for any optional services your config enables (below), then
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. |
Compose profiles for optional services
Two capabilities are wired to a Compose profile, computed automatically from
your config by bootstrap.sh and update.sh — there is no separate
docker compose --profile ... command to run for either:
| Profile | Services | Turned on by |
|---|---|---|
agent-gateway | litellm, litellm-db | AGENT_GATEWAY_ENABLED=true. See Model gateway. |
cloud-workspaces | redis (the cloud-materialization lock) | The cloud sandboxes add-on (coming soon); a current install leaves this profile off. |
When one of these is enabled, bootstrap/update pull, start, and wait for the matching services to report healthy before handing back control. A base install with neither capability configured never pulls or starts them.
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. This mode is for evaluation only.
Bring your own certificate
The shipped Caddyfile always reserves a /llm path for the
model gateway add-on ahead of the
default site handler — harmless when the gateway is off, since there is
simply no litellm host to resolve:
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), add a tls directive pointing at your
own certificate, keeping the rest of the file intact:
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), including any optional-profile services
your config enables (the model gateway, cloud-workspaces Redis) — the same
mechanism bootstrap uses, so there is nothing extra to run for those. 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.
- Model gateway
- Updates & versioning
- Troubleshoot