Skip to content

Install on your own server

For the person who runs servers at your company. This guide runs the Portward gateway with Docker Compose on a Linux machine in your own network, in about 30 minutes. On AWS, Azure or Google Cloud, a template sets up the same gateway for you: see Pick a cloud.

Start with Get started, steps 1 to 3: by then you have your license key (pwlic_…) and your gateway’s address.

  • A small Linux server with Docker, in the network that can reach your databases (1 CPU and 1 GB of memory is enough to start). A cloud VM is fine.
  • An address for the gateway. By default it is https://<name>.gw.portward.ai, which reaches the server through the Portward relay with no inbound ports. Or use a name of your own, such as portward.yourcompany.com. Then ports 80 and 443 must be open to the internet, because Claude and ChatGPT connect from their own servers. The rest of this guide writes portward.yourcompany.com for your address.
  • Your company directory: Microsoft Entra, Okta, Google Workspace, or any OpenID Connect directory. Without one, a sign-in provider such as WorkOS AuthKit works instead (step 1b).
  • Read-only logins for the databases you’ll connect. Portward refuses a login that can write.

1. Set up sign-in with your directory (10 minutes)

Section titled “1. Set up sign-in with your directory (10 minutes)”

Register one app for the gateway in your directory, with redirect address https://portward.yourcompany.com/auth/callback. Sign-in setup has the steps for Entra, Okta and Google. Keep its issuer, client id and secret, and the admin group’s id or name, for step 2.

1b. Or with WorkOS, without a directory (5 minutes)

Section titled “1b. Or with WorkOS, without a directory (5 minutes)”

Set up WorkOS AuthKit as Sign-in setup describes, and keep your AuthKit domain for step 2.

Run one gateway per license key. Don’t start a second install folder with the same key.

On the server, create an empty folder with a name no other install on it uses, and go into it. Docker Compose names the containers and volumes after the folder, so two installs in folders of the same name would share them. Put your license key in a variable, so it isn’t typed into a command line. Then sign in to Portward’s image registry with it and run the installer. With Okta, read “With Okta” below first.

Terminal window
read -rs PORTWARD_LICENSE && export PORTWARD_LICENSE # paste the key, then Enter
printf %s "$PORTWARD_LICENSE" | docker login registry.portward.ai -u portward --password-stdin
docker run --rm -e PORTWARD_LICENSE --user "$(id -u):$(id -g)" -v "$PWD:/install" -w /install \
registry.portward.ai/portward/gateway:1.1.0 init --directory <issuer> --client-id <client id> --admin-group "<group>"

(With WorkOS instead: --issuer https://your-team.authkit.app in place of --directory, --client-id and --admin-group.)

With Okta, init in 1.1.0 refuses Okta’s org server (https://<your org>.okta.com) with “doesn’t publish a plain https jwks_uri”. Give it https://<your org>.okta.com/oauth2/default as <issuer> instead. Then open portward.toml in a text editor. Under [auth], set directory to Okta’s org server, as Sign-in setup uses it, and add a scopes line right under it, because Okta sends groups only when asked for them. Leave the comment lines as they are:

directory = "https://<your org>.okta.com"
scopes = ["groups"]

With a directory, the app’s secret goes in .env, on the empty PORTWARD_DIRECTORY_SECRET= line init wrote. To put it there without typing it into a command line:

Terminal window
read -rs SECRET # paste the app's client secret, then Enter
(umask 077; grep -v '^PORTWARD_DIRECTORY_SECRET=' .env > .env.new && \
printf 'PORTWARD_DIRECTORY_SECRET=%s\n' "$SECRET" >> .env.new) && mv .env.new .env

The installer writes the files below, plus relay.toml and relay.env (your license key, owner-only) for the relay client. To use your own address instead of the relay, add --domain portward.yourcompany.com (then point it at the server and open ports 80 and 443, step 3). It ends with a short list of next steps, which steps 3 to 6 below follow. With a directory, 1.1.0’s list says “add both addresses above” without listing them: they are the admin connector (step 4) and the data connector (step 6). Where the list or portward.toml names docs/sign-in.md or docs/install.md, those are Sign-in setup and this page.

FileWhat it is
portward.tomlThe gateway’s settings. You’ll edit admin_subjects and setup_hosts here.
.envThe gateway key, and with a directory the app’s secret. Back it up and keep it secret: saved connections can’t be opened without the key.
compose.yamlThe gateway, plus Caddy, which gets and renews the HTTPS certificate on its own.
CaddyfileCaddy’s one-line setting for your address.

No inbound ports: the Portward relay (the default)

Section titled “No inbound ports: the Portward relay (the default)”

With the relay, skip the DNS and firewall steps below: the gateway connects out on port 443 and nothing opens inbound. The relay passes your traffic through still encrypted; the certificate stays on your server. To put in a new license key later, see Replace a license key.

  1. With your own address only: point portward.yourcompany.com at the server (a DNS A record) and open ports 80 and 443. (Through the relay, nothing to do.)
  2. docker compose up -d
  3. docker compose exec gateway portward --config /config/portward.toml doctor

doctor checks, from outside the server, what Claude and ChatGPT need: the HTTPS certificate, the sign-in provider, sign-in for both connectors, and the records. It says what to fix.

  1. Add the admin connector, https://portward.yourcompany.com/admin/mcp, in Claude or ChatGPT and sign in. Get started, step 7 shows where to add it.
  2. Members of the group you gave as --admin-group are admins as soon as they sign in. If you have no admin group, as with WorkOS, the gateway refuses your first sign-in because no one is an admin yet. Its log names your sign-in id: docker compose logs gateway | grep "admin connector refused".
  3. Without an admin group, put that id in admin_subjects in portward.toml, then docker compose restart gateway, and connect again.
  1. In portward.toml, list the database hosts admins may connect in setup_hosts (for example ["db.internal.yourcompany.com"], or "*.yourcompany.internal" for a whole domain), then docker compose restart gateway.
    • Object stores on their cloud’s own endpoint (S3, Azure Blob, Cloud Storage), BigQuery, Microsoft 365 and Google Drive need no entry.
    • Snowflake needs its account’s host, such as youraccount.snowflakecomputing.com.
    • A host on a domain other customers share needs its exact name, never a wildcard: an RDS or Redshift endpoint (*.amazonaws.com, such as wg.123456789012.us-east-1.redshift-serverless.amazonaws.com) or a Databricks workspace (such as dbc-1234abcd-5678.cloud.databricks.com).
    • Redshift IAM sign-in without an access key uses the gateway’s own AWS role, which is off until you set PORTWARD_REDSHIFT_GATEWAY_ROLE=1.
  2. Set up the connection from the admin connector and choose what the AI may see: Get started, step 8.
  1. Say who may use the data connector in portward.toml, then docker compose restart gateway.

    • With Entra or Okta, by group. Keep users = [] and list the groups in user_groups. Don’t remove the users line: without it, everyone your directory lets in can ask.
      users = []
      user_groups = ["Portward Users"]
      Entra sends group Object IDs, Okta group names. The group must be assigned to the sign-in app and, with Okta, pass its groups claim filter (Sign-in setup, step 3). Entra doesn’t mark emails as verified, so an email domain doesn’t work there.
    • With Google Workspace or WorkOS, by email.
      users = ["@yourcompany.com"] # everyone whose verified email is at yourcompany.com
      # users = ["user_01ABC", "dana@yourcompany.com"] # or people one by one
      A whole domain needs a verified email in the sign-in token. Google Workspace sends one. In WorkOS, add email and email_verified to the access token’s JWT template; otherwise list people by id.

    With users and user_groups both empty, only admins can ask, which is enough for a first test. In 1.1.0, doctor’s users line counts only the users entries, not the groups. Someone refused is named in the log: docker compose logs gateway | grep "data connector refused".

  2. Everyone adds the data connector, https://portward.yourcompany.com/mcp: Get started, step 9.

  • Update, back up and restore, and automatic updates: Update and back up. Keep .env apart from the data: it holds the gateway key, which backups need and don’t contain.
  • Check the records: docker compose exec gateway portward --config /config/portward.toml records verify confirms no answer record, reveal (a Show of a hidden value) or setup change was altered or removed.
  • Logs: docker compose logs gateway. They name people by their sign-in id and never contain row values.
  • After docker compose restart gateway, the address answers 502 for about 2 seconds, until the gateway is up again.
doctor saysFix
no valid HTTPS certificate yetDNS isn’t pointing at the server yet, or ports 80/443 are closed. docker compose logs https shows Caddy’s attempts.
didn’t answerThe containers aren’t running (docker compose ps), or a firewall blocks the server.
discovery … doesn’t namepublic_url in portward.toml doesn’t match the address people use.
sign-in provider didn’t answerThe directory address (with WorkOS, issuer) in portward.toml is wrong, or the server can’t reach the internet.
nobody is an admin yetStep 4.
anyone … signs in can use the data connectorAdd users = [...] under [auth] (step 6).

If you use WorkOS and Claude can’t connect even though doctor passes, check that both connector addresses are in WorkOS’s Resource Indicators exactly, with https:// and no trailing slash.