Installing on your own server
Captivo Access is Apache 2.0 and ships as published container images. This guide takes one public Linux host from bare to a working console with automatic HTTPS; afterwards the Connector, resources and people are the same as on Cloud.
What gets installed
Four long-running containers on one public host, plus two helpers. The Manager is the console: setup, sign-in, invitations, policy. The Go data-plane terminates the connector tunnel and is the identity-aware proxy vendors actually talk to. Postgres holds the configuration and the audit log. Caddy terminates TLS on ports 80 and 443 and obtains every certificate for you. A one-shot access-migrate service applies the database schema before the Manager starts, and access-cron runs the scheduled jobs.
The Connector is not part of this host. It runs separately, on a machine inside the network that holds the apps, and dials out to this server; see step 4 of the Connector guide once the console is up.
1Server and ports
A Linux host (Ubuntu 22.04 or newer in this guide) with a public IP. Ports 80 and 443 must be reachable from the internet: Let's Encrypt validates every certificate over them. Nothing else needs to be open inbound; the Connector, later, only connects outward.
apt update && apt -y upgrade
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable2Install Docker
Docker Engine with Compose v2, the docker compose subcommand rather than the old docker-compose binary.
curl -fsSL https://get.docker.com | sh
docker version
docker compose version3DNS records
Pick the subdomain everything lives under, for example access.example.com, and create three A records at the server's public IP. The third is a wildcard: one record that covers every vendor app you will ever publish, so DNS is never touched again per app.
| Type | Name | Value |
|---|---|---|
| A | manager.access.example.com | server IP |
| A | connect.access.example.com | server IP |
| A | *.access.example.com | server IP |
Wait for the records to resolve before continuing: dig +short manager.access.example.com and dig +short anything.access.example.com should both print the server IP; the second proves the wildcard.
4Deploy files and .env
Clone the repository on the server and move into the deploy directory. The fastest path is the setup script: it derives the cookie domain, the public URL and the passkey relying-party ID from the domain you give it, generates every secret once, pulls the published images and starts the stack. It is idempotent, so re-running it is safe.
git clone https://github.com/captivo-io/captivo-access.git
cd captivo-access/deploy
./setup.sh access.example.comIf you prefer to fill the file by hand, copy .env.prod.example to .env and generate each secret with openssl rand -hex 32. The variables that matter:
| Variable | Example | What it is |
|---|---|---|
| ACCESS_DOMAIN | access.example.com | The base domain everything is served under. |
| COOKIE_DOMAIN | .access.example.com | Leading dot, so the sign-in cookie also works on every app hostname. Without it vendors sign in and bounce back to login. |
| MANAGER_PUBLIC_URL | https://manager.access.example.com | The console's public address, used for login redirects. |
| WEBAUTHN_RP_ID | access.example.com | The bare access domain, not manager. Must match the host in the browser or passkeys fail. |
| POSTGRES_PASSWORD | openssl rand -hex 32 | Database password. |
| SESSION_SECRET | openssl rand -hex 32 | Signs session tokens. |
| ENCRYPTION_KEY | openssl rand -hex 32 | Encrypts recovery secrets at rest. |
| DATAPLANE_SECRET | openssl rand -hex 32 | Shared secret between the Manager and the data-plane. |
| CRON_SECRET | openssl rand -hex 32 | Authorises the scheduled-job endpoints. |
| AUDIT_RETENTION_DAYS | 730 | Initial audit-log retention; editable later on the Policy page. |
The four capability flags RECORDING_ENABLED, NATIVE_GATEWAY, VAULT_ENABLED and ISOLATED_ENABLED are on by default; leave them commented out, and set one to 0 only to switch that capability off. Most operational settings (retention, session limits, the allowlist, the webhook) are managed later from Infrastructure → Policy, where the value set in the console always wins over the environment.
5Bring the stack up
If you used the setup script this already happened. Otherwise start the stack and watch Caddy obtain the first two certificates, for the manager and connect hostnames; that is why ports 80 and 443 had to be open.
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f caddyYou run no database migration by hand. The access-migrate service applies the schema on every up -d, is a no-op when nothing changed, and refuses any destructive change so it cannot silently drop data. If the Manager does not start, read docker compose -f docker-compose.prod.yml logs access-migrate first.
Scheduled jobs are automatic too: the access-cron container checks resource reachability every five minutes and runs audit retention, recording retention and the audit anchor once a day. No host crontab is needed.
6Create the first admin
Open https://manager.access.example.com/setup and register the first account with a passkey: Touch ID, Windows Hello or a security key. That account becomes the Admin. There is no default or seed account, and there are no passwords.
WEBAUTHN_RP_ID not matching the host in the address bar. See Cannot sign in with a passkey.7Verify DNS and TLS from the console
In the console, open Infrastructure → Custom domain. It shows the exact wildcard record for your domain and your server's IP, with a Verify DNS button. A green result means the wildcard is live and every app you publish will get HTTPS on its first visit. Red means the *.access record from step 3 is missing or has not propagated yet.
From here on the path is the same as on Cloud: install a Connector inside your network, define resources, invite people. If an app address later shows a certificate error or a 502, start with The app does not open.
Where recordings are stored
Not on this server. Session recordings (screen streams, keystroke logs, isolated-browser video) are written to the Connector's own disk under /data, encrypted with AES-256-GCM using a key the Connector generates on first start and keeps at /data/recording.key. The control plane never receives the content and cannot decrypt it. Four consequences follow:
- Back up the Connector's volume. Nobody else has a copy; if the volume is lost the recordings are gone.
- Back up recording.key with it. Without the key the recordings cannot be read, and it cannot be reissued.
- A recording is playable and searchable only while its Connector is online.
- An erasure completes when the Connector next connects; until then the recording stays marked pending.
The rest, including retention and the deletion flow, is in Session recording and live view.
Updating
The console's Infrastructure → Updates page tells you when a new release exists and shows the exact commands. On the server they are:
cd captivo-access/deploy
git pull
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker image prune -a -f --filter label=org.opencontainers.image.title=captivo-accessThe schema migrates automatically on up -d. The prune line reclaims the disk of the replaced images; -a is required because the compose file pins versions, so a replaced image keeps its tag and a plain prune reclaims nothing. The version you are running is never removed. Connectors live on other hosts and are not touched by this: update each by pulling the new connector image and recreating the container; the token in /data persists, so no re-pairing is needed.
For product-wide problems: The connector shows offline · The app does not open · Cannot sign in with a passkey