Skip to content

Deploy & Verify

Work through Security Hardening, Database Configuration, and Resources & Scaling first — the schema applied, the secrets provisioned, and the configuration assembled. This page installs the result and confirms it.

Provision the secrets

The chart refuses to render without them, so this comes first:

./generate-secrets.sh api-portal my-release   # <namespace> [release-name]

It writes values-secrets.yaml, naming each Secret it created along with the has* flags for whichever optional keys it provisioned. Keep that file alongside your own values and pass both at install time.

Run the setup script once, on the first VM only:

./scripts/setup.sh

It provisions the TLS pair under resources/certificates/ and the portal's encryption key and session secret under resources/keys/, and writes .env and api-platform.env.

Then copy the generated material to every other VM before starting them, so all instances share it:

rsync -a resources/keys/ resources/certificates/ <other-vm>:<install-dir>/resources/

Do not run setup.sh independently on each VM — it would generate a different key and session secret per host, which is the failure described in Security Hardening.

Deploy

The api-portal chart pulls its components as OCI dependencies, so fetch them before installing:

helm dependency update ./api-portal-helm-chart

Then install with your production values last, so they win:

helm install my-release ./api-portal-helm-chart \
  --namespace api-portal --create-namespace \
  -f values-secrets.yaml \
  -f values-production.yaml

Render before you install if you want to see what a values change actually produces — this is also where the chart's fail-fast guards fire, before anything is created:

helm template my-release ./api-portal-helm-chart \
  -f values-secrets.yaml -f values-production.yaml > /dev/null

A missing secrets.existingSecret or tls.certificateProvider: selfSigned fails here with an explicit message.

On each VM, from the distribution directory:

docker compose up -d

COMPOSE_PROFILES in .env decides what starts. For a standalone portal against an external database, that is api-portal alone.

Check the configuration was read as you intended before putting the VM into the load balancer pool — the startup log reports the resolved database driver and the authorization mode:

docker compose logs api-portal | head -40

Verify

Confirm every instance is serving, not just that the deploy command succeeded.

kubectl -n api-portal get pods -l app.kubernetes.io/name=api-portal-ui
kubectl -n api-portal get hpa,pdb

Check the pods landed on different nodes — a topology-spread constraint that couldn't be satisfied leaves a pod Pending:

kubectl -n api-portal get pods -o wide -l app.kubernetes.io/name=api-portal-ui

Confirm the portal is serving, from inside the cluster:

kubectl -n api-portal exec deploy/my-release-api-portal-ui -- \
  curl -sk https://localhost:9543/health

On each VM:

docker compose ps
curl -sk https://localhost:9543/health

Then confirm the load balancer sees every instance as healthy, and that its health check is pointed at /health rather than /.

Then verify the two things a single-instance smoke test cannot catch, on either substrate:

Sessions are shared. Log in through the load balancer or ingress, then stop the instance that served the login. The next request should land on another instance with the session intact. If you are logged out instead, the instances are not reading the same session table — check that every instance points at the same external database and resolves the same encryption key and session secret.

Only one instance dispatches each event. Trigger a credential change and confirm the registered subscriber receives exactly one delivery, not one per instance. See Control Plane Connection.

Upgrade

helm dependency update ./api-portal-helm-chart
helm diff upgrade my-release ./api-portal-helm-chart \
  -f values-secrets.yaml -f values-production.yaml    # requires the helm-diff plugin
helm upgrade my-release ./api-portal-helm-chart \
  -f values-secrets.yaml -f values-production.yaml

Always pass the secrets values file. Omitting it on an upgrade fails the render rather than silently dropping the Secret reference — intended behavior, but it does mean an upgrade command is no shorter than an install command.

Roll back a bad release with helm rollback my-release.

Upgrade one VM at a time so the others keep serving:

  1. Remove the VM from the load balancer pool.
  2. Update the image tag in docker-compose.yaml, then docker compose up -d to recreate the container.
  3. Confirm https://localhost:9543/health responds, and check the startup log.
  4. Return it to the pool, then move to the next VM.

Re-running setup.sh is safe — it fills in only what is missing and never overwrites an existing value — so an upgrade won't rotate the encryption key out from under stored credentials.

Roll back by putting the previous image tag back and recreating the container.

Two things hold on both substrates:

  • Old and new instances overlap during a rolling upgrade, so peak database connections briefly exceed instances × max_open_conns. Leave headroom for it.
  • A rollback reverts configuration, not database schema changes. Check the release notes before upgrading across a version that migrates the schema.