Self-Hosted Deployment

A self-hosted deployment runs the same product as cloud in a Docker Compose stack on servers you control. Your users still sign in through the portal, and the stack checks in with the portal for licensing. Boards, sources, chat, and the database stay inside your network.

Important

Self-hosting is available on the Enterprise plan. Once your organization is on Enterprise, our team provisions and approves an on-premises deployment for it. That approval is what unlocks the Self-hosted page in the portal and lets your servers pull images. Use the button above to start the conversation.

How it works

The container images live in a private registry. Your host trades its deployment credentials for 12 hours of pull access, pulls the release the portal names, and runs it. Once running, the stack sends the portal an hourly heartbeat and keeps working for up to 24 hours if the portal is unreachable.

How a self-hosted deployment gets access, pulls, runs and reports in.

Pull access is tied to your subscription. If the subscription lapses, running containers keep running, but the next sign-in is refused and you cannot pull a new release until billing is resolved.

What you'll need

ResourceMinimum
CPU2 cores (4+ for teams over 10)
RAM4 GB (8 GB+ recommended)
Disk20 GB free
OSLinux x86_64, macOS 12+, or Windows 10/11 with Docker Desktop
Docker20.10+ with Compose v2
Toolscurl and jq on Linux and macOS (the registry sign-in script uses them)
DatabasePostgreSQL 14+, reachable from the host — see the note below

Images are built for linux/amd64. Docker Desktop on Apple Silicon runs them under emulation, which is fine for evaluation. Linux ARM64 servers are not supported.

Important

PostgreSQL is bring-your-own. The compose file does not run a database for you. Before starting the stack, prepare a reachable managed or self-run instance and its credentials.

Outbound access

The host needs HTTPS (port 443) to all three of these. Enterprise firewalls routinely allow the first two and forget the third, and the symptom is a pull that stalls at 0%.

DestinationPurpose
1yv8vsjuzd.execute-api.us-east-1.amazonaws.comPortal API: registry sign-in, licensing, heartbeat
833239388352.dkr.ecr.us-east-1.amazonaws.comImage registry: manifests and authentication
*.s3.us-east-1.amazonaws.comImage layers, served from S3 in us-east-1

The pre-flight script in the install steps checks all three before you download anything.

Get your deployment credentials

  1. Our team provisions an on-premises deployment for your organization and approves it. Until then, the portal's Self-hosted page shows a contact card instead of the installer.
  2. Sign in to portal.provenmap.com as an organization admin and open Self-hosted.
  3. Select Create Secret. Copy the App ID and the secret into your secrets manager.
Warning

The secret is shown once. Recreate Secret on the same page invalidates the old one for both the heartbeat and registry sign-in. After recreating it, update .env and run the sign-in script again.

Install

The steps below take about ten minutes on a host that meets the requirements.

1. Check the host. The script verifies Docker, memory, disk, port 80, jq, and reachability of all three outbound hosts.

BASH
curl -fsSL https://raw.githubusercontent.com/syntaxia/provenmap/main/deploy/preflight.sh | bash

2. Download the kit. This creates ~/provenmap with the compose file, nginx.conf, .env.template, and the sign-in script.

BASH
curl -fsSL https://raw.githubusercontent.com/syntaxia/provenmap/main/deploy/install.sh | bash
cd ~/provenmap

3. Configure. Copy the template if the installer didn't, then set the three required values.

BASH
cp -n .env.template .env
nano .env
BASH
PMAP_APP_ID=cd_your_app_id
PMAP_APP_SECRET=ck_live_your_secret
PMAP_DATABASE_PASSWORD=YourSecurePassword123!
PMAP_DATABASE_HOST=db.internal.example.com

4. Sign in to the image registry. The script trades your credentials for 12 hours of pull access and, on first run, pins PMAP_VERSION to the current release.

BASH
./registry-login.sh
TEXT
  ok      Signed in to 833239388352.dkr.ecr.us-east-1.amazonaws.com
  ok      Pull access until 2026-09-02T21:14:09.000Z
  ok      Pinned PMAP_VERSION=1.4.8 in .env

5. Start the stack.

BASH
docker compose -f docker-compose.production.yml pull
docker compose -f docker-compose.production.yml up -d

The first start downloads roughly 1 GB of images and takes two to three minutes.

Tip

Pull access lasts 12 hours. Run the sign-in script again before any docker compose pull. Starting or restarting containers you already have does not need it.

Verify the installation

BASH
docker compose -f docker-compose.production.yml ps
curl http://localhost/health
curl http://localhost/api/status

All three containers (provenmap-proxy, provenmap-web, and provenmap-server) report Up (healthy). The health check returns {"status":"ok"}, and the status endpoint reports "license": { "status": "active" } after the server checks in with the portal.

Open http://localhost (or your configured port), select Sign In, and sign in with your portal account. Every user needs a portal account and membership in your organization.

Configuration

Settings live in .env next to the compose file. Restart the affected container after a change: docker compose -f docker-compose.production.yml up -d --force-recreate server.

Required

VariableValue
PMAP_APP_IDThe App ID from the Self-hosted page
PMAP_APP_SECRETThe secret from the Self-hosted page
PMAP_DATABASE_PASSWORDPassword for your PostgreSQL user
PMAP_VERSIONThe exact release to run, for example 1.4.8. The sign-in script writes it on first run

There is no latest tag. Releases are immutable, your deployment runs exactly the release named here, and nothing changes it but you.

Database

VariableDefaultNotes
PMAP_DATABASE_HOSTlocalhostHostname of your PostgreSQL instance
PMAP_DATABASE_PORT5432
PMAP_DATABASE_USERprovenmap
PMAP_DATABASE_NAMEprovenmap
PMAP_DATABASE_SSLunsetSet to require for managed databases that enforce TLS

Migrations run automatically when the server starts. They are forward-only and hold an advisory lock, so several restarts in a row are safe.

Connection pool and platform behavior
VariableDefaultNotes
PMAP_DATABASE_MAX_CONNECTIONS10Pool size per server container
PMAP_DATABASE_IDLE_TIMEOUT30000Milliseconds before an idle connection closes
PMAP_DATABASE_CONNECTION_TIMEOUT5000Milliseconds to wait for a connection
PMAPP_OFFLINE_TTL86400Seconds cached entitlements stay valid while the portal is unreachable
PMAPP_HEARTBEAT_INTERVAL3600Seconds between heartbeats
PMAPP_LOG_LEVELinfodebug, info, warn, or error
PMAP_EXIT_ON_FAILUREtrueExit at startup when the portal is unreachable and no valid cache exists

Optional

VariableDefaultNotes
PMAP_PORT80External port of the bundled nginx proxy
PMAP_PORTAL_URLhttps://1yv8vsjuzd.execute-api.us-east-1.amazonaws.com/productionThe portal API. Leave the default — https://portal.provenmap.com is the web app and has no API routes

Chat and in-app insight generation use a model provider, connected once from Settings → Connectors. Plugin commands such as /analyze, /sync, and /insights do not require one. Icon storage can be moved from the local filesystem to S3 — see Organization Settings. Plugins and MCP clients point at your instance with PMAP_BASE_URL and PMAP_MCP_URL — see Authentication.

Note

Trial credits to fund early usage before you connect your own model provider are available on request. They are billed to you, not your users.

Upgrade and roll back

The Self-hosted page in the portal shows the current release and whether your deployment is up to date. Upgrading is a decision you make, one line in .env.

BASH
cd ~/provenmap
./registry-login.sh        # prints "Release 1.4.9 is available; you are pinned to 1.4.8"
nano .env                  # PMAP_VERSION=1.4.9
docker compose -f docker-compose.production.yml pull
docker compose -f docker-compose.production.yml up -d

Back up the database first (below). Expect one to two minutes of downtime while containers restart.

To roll back, set PMAP_VERSION to the previous release and run the same pull and start. Database migrations do not roll back; if a release's migration fails, restore your pre-upgrade backup, pin the previous release, and contact us.

Note

We keep recent releases in the registry, not every release ever shipped. A deployment pinned to a release that has aged out cannot pull it onto a new host — upgrade before you migrate hosts, or contact support.

Back up

Two things hold state: your PostgreSQL database and the provenmap-data volume (sessions, cache, and the cached license).

Back the database up with your usual PostgreSQL process. From the host:

BASH
pg_dump -h db.internal.example.com -U provenmap provenmap | gzip > backups/db-$(date +%Y%m%d).sql.gz

Back the data volume up as a tar archive:

BASH
docker run --rm -v provenmap-data:/data -v "$(pwd)/backups:/backup" \
  alpine tar czf /backup/data-$(date +%Y%m%d).tar.gz /data

The volume is rebuilt from the portal on the next start if you lose it; the database is not. Back the database up daily and before every upgrade.

SSL and reverse proxies

The stack ships with an nginx container on port 80. You have two options for HTTPS:

  • Terminate TLS on your load balancer and forward to port 80. Nothing in the stack changes.
  • Replace nginx.conf with your own configuration, or route straight to the containers from your existing proxy (Traefik, Caddy, HAProxy). Use this routing table:
PathDestinationNotes
/api/*server:3001
/healthserver:3001
/platform/*server:3001
/.well-known/*server:3001
/socket.io/*server:3001Needs WebSocket upgrade headers
/*web:3000Frontend, lowest priority

Keep ports 3000 and 3001 off the public internet; only the proxy should reach them.

Troubleshooting

The sign-in script prints FAIL … [code]

The portal refused registry access. The code in brackets says why.

CodeMeaningFix
deployment_not_approvedYour deployment is not approved yetWait for approval; you are emailed when it lands
subscription_inactiveThe subscription does not grant access (past_due, cancelled)Fix billing in the portal. Running containers keep working; new pulls resume once billing is fixed
not_on_premThe credentials belong to a cloud deploymentUse the App ID and secret from the Self-hosted page
deployment_disabledThe deployment was disabledContact support
deletion_pendingThe organization is being deletedContact support if this is unexpected
release_not_publishedNo release is published for self-hostingThis is on our side — contact support
registry_unavailableThe registry did not respondRetry in a few minutes

Invalid deployment credentials means PMAP_APP_ID or PMAP_APP_SECRET is wrong — or the secret was recreated in the portal. Too many requests means more than ten sign-ins a minute from one address; wait a minute.

denied: Your authorization token has expired

Pull access lasts 12 hours from sign-in. Sign in again and retry:

BASH
./registry-login.sh
docker compose -f docker-compose.production.yml pull
… did not answer as the Portal API

PMAP_PORTAL_URL points at https://portal.provenmap.com, which is the web app. Remove the line from .env so the default applies, or set it to the API host from the outbound access table. Then recreate the server container so it picks up the change:

BASH
docker compose -f docker-compose.production.yml up -d --force-recreate server
Pull stalls at 0% after a successful sign-in

The registry answered, but image layers download from S3 and your firewall blocks it. Allow HTTPS to *.s3.us-east-1.amazonaws.com — the regional pattern, not only *.s3.amazonaws.com. Confirm from the host; any HTTP status is fine, a connection error is the problem:

BASH
curl -sI --max-time 10 https://s3.us-east-1.amazonaws.com/ | head -1
The server exits at startup with "Portal unreachable"

The server needs the portal API on its first start and whenever its cached license is older than PMAPP_OFFLINE_TTL. Check outbound access to the API host from inside the container:

BASH
docker compose -f docker-compose.production.yml exec server \
  node -e "fetch(process.env.PMAP_PORTAL_URL + '/platform/health').then(r => console.log(r.status))"

A 200 means the network is fine and the credentials are the problem. Anything else is a firewall or proxy rule.

Port 80 is already in use

Set PMAP_PORT in .env to a free port and run docker compose -f docker-compose.production.yml up -d. The stack is then at http://localhost:<port>.

What's next