# Deploy the Developer Portal

The API7 Developer Portal is made up of two official container images that work together:

| Image                              | Role                                                                                                                                                                                                                                                                         | Port           | Source repository         |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------- |
| `api7/api7-ee-developer-portal`    | Portal API (backend): the Go service that stores portal definitions, API products, developers, applications, subscriptions, and credentials. It shares the main `api7ee` PostgreSQL database with the rest of the control plane.                                             | `4321` (HTTPS) | `api7ee-3-control-plane`  |
| `api7/api7-ee-developer-portal-fe` | Developer Portal frontend: the customer-facing TanStack Start web app that developers sign in to. It uses its own PostgreSQL database for user sessions and developer accounts (via Better Auth and Drizzle), and communicates with the Portal API using a per-portal token. | `3001`         | `api7ee-developer-portal` |

One backend serves your entire organization. You can deploy multiple frontend instances, one per portal, for separate branding or audiences.

The Portal API is part of the control plane and uses the same database as the Dashboard. This guide deploys the frontend and its separate session database, then connects the frontend to the existing Portal API. It also explains how to run another Portal API container when the control plane deployment does not already include one.

If the packaged `api7-ee` directory contains `portal.sh`, follow [Start a Local Developer Portal](https://docs.api7.ai/api7-gateway/developer-portal/get-started.md) for local evaluation. Otherwise, continue with the manual steps in this guide. Use this guide as well for Kubernetes, external databases, multiple portals, or a frontend managed outside the package.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

* API7 Enterprise installed and activated with a portal-enabled license (trial licenses include portal support by default).
* Network connectivity from the frontend host to the Portal API endpoint (`4321/tcp`).
* Docker and Docker Compose, or a Kubernetes cluster with Helm, depending on which deployment method you choose.

## Create a Portal and Token[​](#create-a-portal-and-token "Direct link to Create a Portal and Token")

Before deploying the Developer Portal frontend, create a portal instance and generate a connection token from the API7 Dashboard.

1. Log in to the API7 Dashboard.
2. Navigate to **Provider Portal > Add Portal**.
3. Provide a **Name** and the **Public URL** where the Developer Portal will be accessible (for example, `https://portal.example.com`).
4. After creating the portal, open its **Settings > Tokens** tab.
5. Click **Generate New Token** and save the token (format: `a7prt-xxxxxxxxxxxx`). You will add this token to the frontend configuration in the next section.

info

Each portal has a unique public URL. A single portal can back one frontend instance or multiple frontend replicas that share the same token and user-session database.

## Configure the Frontend[​](#configure-the-frontend "Direct link to Configure the Frontend")

Create a working directory for the frontend configuration:

```
mkdir api7-developer-portal && cd api7-developer-portal

mkdir -p frontend_conf
```

### Frontend Configuration[​](#frontend-configuration "Direct link to Frontend Configuration")

The frontend connects to the Portal API using the token you generated above. It stores authentication and session data in a separate PostgreSQL database.

frontend\_conf/config.yaml

```
portal:

  url: https://YOUR_PORTAL_API_HOST:4321

  token: a7prt-xxxxxxxxxxxx



db:

  url: "postgres://portal:YOUR_PORTAL_DB_PASSWORD@portal-postgres:5432/portal"



auth:

  secret: "REPLACE_WITH_SECURE_RANDOM_STRING"



app:

  baseURL: "https://portal.example.com"

  trustedOrigins:

    - "https://portal.example.com"
```

❶ Set `url` to the existing Portal API endpoint. Its certificate must cover this host name. Use the token generated in the Provider Portal.

❷ Set the PostgreSQL connection string for the authentication and session database used by the frontend.

❸ Generate a secure authentication secret with `openssl rand -base64 32`.

❹ Set the public URL where developers access the portal. Add the same origin to `trustedOrigins`.

caution

Replace `auth.secret` with a securely generated random value before running in any shared environment. Do not use the example value in production.

This file contains the Portal API token, session-database credentials, and authentication secret. Keep it out of version control.

The frontend verifies the Portal API certificate. Use a certificate issued by a CA that the frontend image trusts. For a private CA, mount its CA certificate into the frontend container and set `NODE_EXTRA_CA_CERTS` to the mounted file. Do not disable TLS verification for a production deployment.

For Docker Compose with a private CA, add the CA to the frontend service:

```
# The frontend runs as user ID 65532 and must be able to read this file.

chmod 644 certs/portal-api-ca.crt
```

docker-compose.yaml (private CA excerpt)

```
services:

  api7-developer-portal-frontend:

    environment:

      NODE_EXTRA_CA_CERTS: /app/certs/portal-api-ca.crt

    volumes:

      - ./frontend_conf/config.yaml:/app/apps/site/config.yaml:ro

      - ./certs/portal-api-ca.crt:/app/certs/portal-api-ca.crt:ro
```

## Deploy[​](#deploy "Direct link to Deploy")

Choose the deployment method that matches your environment.

* Docker Compose
* Kubernetes

Docker Compose is the quickest way to run the frontend and its session database for development or testing. The existing Portal API remains part of the control plane.

Create `docker-compose.yaml` in the same directory:

docker-compose.yaml

```
services:

  portal-postgres:

    image: postgres:16

    hostname: portal-postgres

    environment:

      POSTGRES_USER: portal

      POSTGRES_PASSWORD: YOUR_PORTAL_DB_PASSWORD

      POSTGRES_DB: portal

    volumes:

      - portal_postgres_data:/var/lib/postgresql/data

    healthcheck:

      test: ["CMD", "pg_isready", "-U", "portal", "-d", "portal"]

      interval: 5s

      timeout: 5s

      retries: 5

    networks:

      - portal



  api7-developer-portal-frontend:

    image: api7/api7-ee-developer-portal-fe:${PORTAL_FE_VERSION}

    hostname: api7-developer-portal-frontend

    restart: always

    volumes:

      - ./frontend_conf/config.yaml:/app/apps/site/config.yaml:ro

    ports:

      - "3001:3001"

    depends_on:

      portal-postgres:

        condition: service_healthy

    networks:

      - portal



networks:

  portal:

    driver: bridge



volumes:

  portal_postgres_data:
```

After you finish editing the file, give the frontend user ownership. Compose preserves the host owner and mode:

```
# The frontend runs as user ID 65532.

sudo chown 65532 frontend_conf/config.yaml

# Mode 600 keeps the token and database password private.

sudo chmod 600 frontend_conf/config.yaml
```

Set the frontend image tag, then start the stack:

```
export PORTAL_FE_VERSION=v0.15.3

docker compose up -d
```

Frontend `v0.15.3` is the version released with API7 Gateway 3.10.9. The Portal API follows the control-plane release version and is already running in the existing control plane.

Check that PostgreSQL reports `healthy` and the frontend reports `Up`:

```
docker compose ps
```

### Run the Portal API separately[​](#run-the-portal-api-separately "Direct link to Run the Portal API separately")

If the control plane deployment does not run the Portal API, you can run the matching `api7/api7-ee-developer-portal:v3.10.9` image separately. Point its `database.dsn` at the existing control-plane database that contains the portal and token you created earlier. Do not create a new empty `api7ee` database for this service: the token is stored in the control-plane database and authentication fails when the Portal API cannot read it.

Create the directories for the backend configuration and TLS files:

```
mkdir -p backend_conf certs
```

Create `backend_conf/conf.yaml` with the existing database DSN and your TLS files:

backend\_conf/conf.yaml

```
server:

  listen:

    host: "0.0.0.0"

    port: 4321

    tls:

      enabled: true

      key_file: /app/certs/tls.key

      cert_file: /app/certs/tls.crt

  status:

    disable: false

    host: "127.0.0.1"

    port: 4322

  pprof:

    enable: false

    host: "127.0.0.1"

    port: 6060

database:

  dsn: "postgres://api7ee:YOUR_DB_PASSWORD@YOUR_CONTROL_PLANE_DB:5432/api7ee"

security:

  ssrf_protection:

    enable: false

    allow_list: []

    deny_list: []
```

❶ The status listener remains available on loopback for local health checks.

❷ The pprof listener is enabled on loopback by default; this example disables it. Enable it only for diagnostics and keep it private.

❸ Point the Portal API at the existing control-plane database that contains the portal and token.

❹ SSRF protection is disabled by default. Enable it to restrict outbound DCR and approval-webhook requests. Its allow and deny entries are CIDRs, and invalid CIDRs prevent startup.

Add the service to `docker-compose.yaml`:

docker-compose.yaml (separate Portal API excerpt)

```
services:

  api7-developer-portal-backend:

    image: api7/api7-ee-developer-portal:v3.10.9

    volumes:

      - ./backend_conf/conf.yaml:/usr/local/api7/conf/conf.yaml:ro

      - ./certs:/app/certs:ro

    command:

      - -c

      - /usr/local/api7/conf/conf.yaml

    ports:

      - "4321:4321"

    networks:

      - portal
```

Place the key and certificates in the certs directory before starting the service:

```
# The Portal API runs as user ID 65532.

sudo chown 65532 certs/tls.key

# Mode 600 is readable only by that owner.

sudo chmod 600 certs/tls.key

# Leave the certificate and CA readable by the Portal API and the frontend.

chmod 644 certs/tls.crt certs/portal-api-ca.crt
```

Make the issuing CA available to the frontend through `NODE_EXTRA_CA_CERTS`. If both containers use the `portal` network, set `portal.url` to the backend service name covered by that certificate, such as `https://api7-developer-portal-backend:4321`.

On Kubernetes, the main `api7/api7ee3` control plane chart deploys the Portal API backend when `developer_portal_configuration.enable: true` (the default). You normally only need to deploy the frontend separately. This tab shows how to enable the backend in the control plane chart, then deploy the frontend as its own workload.

### Enable the Backend in the Control Plane Chart[​](#enable-the-backend-in-the-control-plane-chart "Direct link to Enable the Backend in the Control Plane Chart")

In your control plane values file (for example, `cp-values.yaml`), ensure the Developer Portal backend is enabled and configured:

cp-values.yaml (excerpt)

```
developer_portal:

  replicaCount: 1

  keyCertSecret: api7-developer-portal-tls

  image:

    repository: api7/api7-ee-developer-portal

    tag: "v3.10.9"



developer_portal_service:

  type: ClusterIP

  port: 4321



developer_portal_configuration:

  enable: true

  server:

    listen:

      host: "0.0.0.0"

      port: 4321

      tls:

        enabled: true

        key_file: /app/certs/tls.key

        cert_file: /app/certs/tls.crt

    status:

      host: "127.0.0.1"

      port: 4322

  log:

    level: warn
```

This excerpt intentionally does not replace `developer_portal_configuration.database.dsn`. Merge it into the existing control-plane values. The Portal API must keep the same chart-local or external database used by the Dashboard and DP Manager.

Create the TLS Secret before applying the chart values. The certificate must cover the Portal API host name used by the frontend. Include its issuing CA so the frontend can trust it:

```
kubectl create secret generic api7-developer-portal-tls \

  --from-file=tls.key=portal-api-tls.key \

  --from-file=tls.crt=portal-api-tls.crt \

  --from-file=ca.crt=portal-api-ca.crt \

  -n api7
```

Install or upgrade the control plane chart with these values:

```
helm upgrade --install api7ee3 api7/api7ee3 \

  --version "3.10.12" \

  -f cp-values.yaml \

  -n api7
```

The backend is now reachable through its Service. Use a DNS name covered by its certificate in the frontend `portal.url`.

### Deploy the Frontend[​](#deploy-the-frontend "Direct link to Deploy the Frontend")

The published `developer-portal-fe` chart targets frontend `0.14.0` and does not match the released `v0.15.3` frontend used here. Deploy this frontend version as a standard Kubernetes workload instead. The manifest below uses a Secret to mount the `config.yaml` you created earlier.

Before applying the manifest, provision the frontend PostgreSQL database and a host or Kubernetes Service reachable as `YOUR_PORTAL_DB_HOST`. Create the `portal` database and user represented by the DSN. Replace the Portal API host and token, database host and password, authentication secret, public URL, and trusted origin in the Secret through your approved secret-management workflow.

developer-portal-frontend.yaml

```
apiVersion: v1

kind: Secret

metadata:

  name: developer-portal-frontend-config

  namespace: api7

type: Opaque

stringData:

  config.yaml: |

    portal:

      url: https://YOUR_PORTAL_API_HOST:4321

      token: a7prt-xxxxxxxxxxxx

    db:

      url: "postgres://portal:YOUR_PORTAL_DB_PASSWORD@YOUR_PORTAL_DB_HOST:5432/portal"

    auth:

      secret: "REPLACE_WITH_SECURE_RANDOM_STRING"

    app:

      baseURL: "https://portal.example.com"

      trustedOrigins:

        - "https://portal.example.com"

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: developer-portal-frontend

  namespace: api7

spec:

  replicas: 2

  selector:

    matchLabels:

      app: developer-portal-frontend

  template:

    metadata:

      labels:

        app: developer-portal-frontend

    spec:

      containers:

        - name: frontend

          image: api7/api7-ee-developer-portal-fe:v0.15.3

          ports:

            - containerPort: 3001

          env:

            - name: NODE_EXTRA_CA_CERTS

              value: /app/apps/site/portal-api-ca.crt

          volumeMounts:

            - name: config

              mountPath: /app/apps/site/config.yaml

              subPath: config.yaml

              readOnly: true

            - name: portal-api-ca

              mountPath: /app/apps/site/portal-api-ca.crt

              subPath: ca.crt

              readOnly: true

      volumes:

        - name: config

          secret:

            secretName: developer-portal-frontend-config

        - name: portal-api-ca

          secret:

            secretName: api7-developer-portal-tls

---

apiVersion: v1

kind: Service

metadata:

  name: developer-portal-frontend

  namespace: api7

spec:

  selector:

    app: developer-portal-frontend

  ports:

    - port: 80

      targetPort: 3001

  type: ClusterIP
```

❶ Node reads the extra CA file when it starts. This path matches the CA mount below.

❷ Mount the frontend configuration from the `config.yaml` key in `developer-portal-frontend-config`.

❸ Mount the `ca.crt` key into a directory that exists in the frontend image.

❹ The configuration volume references the Secret created in the first document of this manifest.

❺ The CA volume references the TLS Secret created for the Portal API.

Apply the manifest after creating `api7-developer-portal-tls` above:

```
kubectl apply -f developer-portal-frontend.yaml
```

Expose the frontend `Service` through an Ingress or LoadBalancer so external developers can reach `app.baseURL`.

note

The manifest stores the complete frontend configuration in a Kubernetes Secret because it contains the Portal API token, database password, and authentication secret. Kubernetes Secrets are Base64-encoded rather than encrypted by default. Do not commit populated values, restrict Secret access, enable encryption at rest, and use your approved secret-management workflow in production.

## Verify the Deployment[​](#verify-the-deployment "Direct link to Verify the Deployment")

1. Open the public URL you configured (`app.baseURL`) in a browser, for example, `https://portal.example.com`.
2. Verify that the home page loads and the **Sign Up** button is visible.
3. Create a test developer account to confirm authentication is functional.
4. Navigate to **API Hub** to confirm the frontend can reach the Portal API. If API products have been published to this portal, they will appear here.

## Deploy Multiple Portal Instances[​](#deploy-multiple-portal-instances "Direct link to Deploy Multiple Portal Instances")

You can deploy multiple Developer Portal frontends against the same backend. Each frontend can serve a different audience, such as public developers, internal teams, or partners:

1. In the Provider Portal, create an additional portal with a unique name and public URL.
2. Generate a new token for that portal.
3. Deploy another frontend instance (a new `Deployment` on Kubernetes or a separate Compose stack) using the new token and public URL in its `config.yaml`.

API products are published to specific portals. An API product published to one portal is not visible on another unless you explicitly publish it there as well.

## Next Steps[​](#next-steps "Direct link to Next Steps")

* [Configure the Developer Portal](https://docs.api7.ai/api7-gateway/developer-portal/deploy/configure-portal.md) to set up authentication, public access, and other runtime settings.
* [Customize the Developer Portal](https://docs.api7.ai/api7-gateway/developer-portal/deploy/customize-portal.md) to apply your branding and extend the frontend.
* [Manage API Products](https://docs.api7.ai/api7-gateway/developer-portal/manage/manage-api-products.md) to publish APIs for developer consumption.
