DOCUMENTATION

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.

Nothing to install? If you would rather we run the control plane, start with Getting started on Cloud instead; you still run only the Connector.

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.

bash
apt update && apt -y upgrade
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

2Install Docker

Docker Engine with Compose v2, the docker compose subcommand rather than the old docker-compose binary.

bash
curl -fsSL https://get.docker.com | sh
docker version
docker compose version

3DNS 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.

TypeNameValue
Amanager.access.example.comserver IP
Aconnect.access.example.comserver IP
A*.access.example.comserver IP
A wildcard DNS record is not a wildcard certificate, and you need no DNS API token. Caddy issues a certificate per app the first time its hostname is visited, and only for hostnames that are real resources in the console. Any DNS provider works.

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.

bash
git clone https://github.com/captivo-io/captivo-access.git
cd captivo-access/deploy
./setup.sh access.example.com

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

VariableExampleWhat it is
ACCESS_DOMAINaccess.example.comThe base domain everything is served under.
COOKIE_DOMAIN.access.example.comLeading dot, so the sign-in cookie also works on every app hostname. Without it vendors sign in and bounce back to login.
MANAGER_PUBLIC_URLhttps://manager.access.example.comThe console's public address, used for login redirects.
WEBAUTHN_RP_IDaccess.example.comThe bare access domain, not manager. Must match the host in the browser or passkeys fail.
POSTGRES_PASSWORDopenssl rand -hex 32Database password.
SESSION_SECRETopenssl rand -hex 32Signs session tokens.
ENCRYPTION_KEYopenssl rand -hex 32Encrypts recovery secrets at rest.
DATAPLANE_SECRETopenssl rand -hex 32Shared secret between the Manager and the data-plane.
CRON_SECRETopenssl rand -hex 32Authorises the scheduled-job endpoints.
AUDIT_RETENTION_DAYS730Initial 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.

Never commit .env. It holds every secret of the deployment.

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.

bash
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 caddy

You 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.

Passkey registration fails with a generic error? It is almost always 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:

  1. Back up the Connector's volume. Nobody else has a copy; if the volume is lost the recordings are gone.
  2. Back up recording.key with it. Without the key the recordings cannot be read, and it cannot be reissued.
  3. A recording is playable and searchable only while its Connector is online.
  4. 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:

bash
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-access

The 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

Was this article helpful?