Files
website/helm/ezscale-website/README.md
Andrew d94a196300 docs(helm): chart README + APP_KEY/Passport bootstrap procedure
Spells out the one-time secret generation that must NEVER be re-run.
Documents local k3d setup and operations runbooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:56:58 -04:00

5.5 KiB

ezscale-website Helm Chart

Production deployment for the EZSCALE billing platform Laravel app.

Environments

Env Values file Notes
Local k3d values-local.yaml Self-contained: in-cluster MariaDB + Valkey. No ingress (use port-forward).
US prod values-us-prod.yaml Connects to existing mariadb CR in ezscale ns; per-app Valkey on Longhorn.

Prerequisites (us-prod)

The ezscale namespace must already have:

  • mariadb-operator installed cluster-wide (provides MariaDB/Database/User/Grant CRDs)
  • A MariaDB CR named mariadb
  • cert-manager ClusterIssuer named letsencrypt
  • Traefik with cloudflarewarp and http-to-https middlewares in kube-system
  • Image-pull Secret named gitea-registry

These are all already provisioned per infrastructure/kubernetes/.

First-time bootstrap (us-prod)

This step is run once per environment, never repeated. The Secret it creates lives forever — re-running it would invalidate every encrypted DB column and every issued OAuth token.

# 1. Generate APP_KEY locally (any Laravel checkout works)
APP_KEY=$(cd /tmp && composer create-project laravel/laravel _key_gen --quiet --no-install \
  && cd _key_gen && composer install --quiet \
  && php artisan key:generate --show)

# 2. Generate Passport keys
TMP_KEYS=$(mktemp -d)
openssl genrsa -out "$TMP_KEYS/oauth-private.key" 4096
openssl rsa -in "$TMP_KEYS/oauth-private.key" -pubout -out "$TMP_KEYS/oauth-public.key"

# 3. Gather production secrets. Read each one and store in shell vars.
DB_PASS=$(openssl rand -base64 32 | tr -d '/=+' | cut -c1-32)
read -sp "STRIPE_KEY (pk_live_...): " STRIPE_KEY; echo
read -sp "STRIPE_SECRET (sk_live_...): " STRIPE_SECRET; echo
read -sp "STRIPE_WEBHOOK_SECRET (whsec_...): " STRIPE_WEBHOOK; echo
read -sp "PAYPAL_CLIENT_ID: " PAYPAL_ID; echo
read -sp "PAYPAL_CLIENT_SECRET: " PAYPAL_SECRET; echo
read -sp "AWS_ACCESS_KEY_ID (Storj): " AWS_KEY; echo
read -sp "AWS_SECRET_ACCESS_KEY (Storj): " AWS_SECRET; echo
read -sp "MAIL_USERNAME (SMTP): " MAIL_USER; echo
read -sp "MAIL_PASSWORD (SMTP): " MAIL_PASS; echo
read -sp "VIRTFUSION_API_KEY: " VF_KEY; echo
read -sp "PTERODACTYL_APP_KEY: " PTERO_KEY; echo

# 4. Create the Secret in the ezscale namespace
kubectl create secret generic ezscale-website-secrets \
  --namespace ezscale \
  --from-literal=APP_KEY="$APP_KEY" \
  --from-literal=DB_PASSWORD="$DB_PASS" \
  --from-literal=STRIPE_KEY="$STRIPE_KEY" \
  --from-literal=STRIPE_SECRET="$STRIPE_SECRET" \
  --from-literal=STRIPE_WEBHOOK_SECRET="$STRIPE_WEBHOOK" \
  --from-literal=PAYPAL_CLIENT_ID="$PAYPAL_ID" \
  --from-literal=PAYPAL_CLIENT_SECRET="$PAYPAL_SECRET" \
  --from-literal=AWS_ACCESS_KEY_ID="$AWS_KEY" \
  --from-literal=AWS_SECRET_ACCESS_KEY="$AWS_SECRET" \
  --from-literal=MAIL_USERNAME="$MAIL_USER" \
  --from-literal=MAIL_PASSWORD="$MAIL_PASS" \
  --from-literal=VIRTFUSION_API_KEY="$VF_KEY" \
  --from-literal=PTERODACTYL_APP_KEY="$PTERO_KEY" \
  --from-file=oauth-private.key="$TMP_KEYS/oauth-private.key" \
  --from-file=oauth-public.key="$TMP_KEYS/oauth-public.key"

# 5. Wipe the local key copies
rm -rf /tmp/_key_gen "$TMP_KEYS"

After this, helm install and every subsequent helm upgrade reference the existing Secret without ever rewriting it.

Deploy

helm upgrade --install ezscale-website ./helm/ezscale-website \
  --namespace ezscale \
  -f helm/ezscale-website/values-us-prod.yaml \
  --set image.tag=v0.1.0

Local dev cluster

# 1. Create cluster with Longhorn (or local-path) and the operator
k3d cluster create ezscale-local
helm repo add mariadb-operator https://helm.mariadb.com/mariadb-operator
helm install mariadb-operator -n mariadb-operator --create-namespace mariadb-operator/mariadb-operator

# 2. Build the image into the cluster
docker build --target app -t ezscale-website:app-local .
docker build --target horizon -t ezscale-website:horizon-local .
docker build --target scheduler -t ezscale-website:scheduler-local .
k3d image import ezscale-website:app-local ezscale-website:horizon-local ezscale-website:scheduler-local -c ezscale-local

# 3. Edit values-local.yaml — replace APP_KEY with `php artisan key:generate --show` output

# 4. Install
helm upgrade --install ezscale-website ./helm/ezscale-website \
  --namespace ezscale --create-namespace \
  -f helm/ezscale-website/values-local.yaml \
  --set image.registry=docker.io \
  --set image.repository=library/ezscale-website \
  --set image.tag=local

# 5. Port-forward and visit http://localhost:8080
kubectl port-forward -n ezscale svc/ezscale-website 8080:80

Operations

# Tail Horizon logs
kubectl logs -n ezscale -l app.kubernetes.io/component=horizon -f

# Run an artisan command in a one-off pod
kubectl run -n ezscale --rm -it --image=git.ezscale.cloud/ezscale/website:app-v0.1.0 \
  --env-from='[{"configMapRef":{"name":"ezscale-website-env"}},{"secretRef":{"name":"ezscale-website-secrets"}}]' \
  artisan-shell -- bash

# Force a Horizon rolling restart
kubectl rollout restart deployment -n ezscale ezscale-website-horizon

What's NOT in this chart (intentionally)

  • Cloudflare Zero Trust for the admin panel — layered on later.
  • ExternalDNS records — DNS managed manually in Cloudflare Terraform.
  • Backup CronJob for the application database — covered by the existing mariadb instance's backup CronJob in infrastructure/kubernetes/ezscale/mysql/.
  • Backups for Storj uploads — Storj provides its own replication; per-bucket lifecycle policies live in the Storj console.