Kubernetes Deployment
AutoCom uses Kustomize overlays for environment-specific Kubernetes deployments. One set of base manifests, different configs per environment.
Directory Structure
k8s/
├── base/ # Shared manifests (all environments)
│ ├── kustomization.yaml
│ ├── api.yaml # Octane + FrankenPHP
│ ├── horizon.yaml # Queue worker
│ ├── frontend.yaml # Next.js
│ ├── nginx.yaml # Reverse proxy
│ └── redis.yaml # Cache/sessions/queues
├── overlays/
│ ├── local/ # Docker Desktop / K3s dev
│ │ └── kustomization.yaml
│ ├── staging/ # Staging environment
│ │ ├── kustomization.yaml
│ │ └── ingress.yaml
│ └── production/ # Production
│ ├── kustomization.yaml
│ ├── ingress.yaml
│ └── hpa.yaml
└── local/ # Standalone manifests (pre-Kustomize)
├── deploy.sh
├── backup.yaml
└── ...
Quick Deploy
# Local (Docker Desktop K8s or K3s)
kustomize build k8s/overlays/default | kubectl apply -f -
# Staging
kustomize build k8s/overlays/default | kubectl apply -f -
# Production
kustomize build k8s/overlays/default | kubectl apply -f -
Environment Differences
| Config | Local | Staging | Production |
|---|---|---|---|
| API replicas | 1 | 2 | 3 (HPA 3→10) |
| Frontend replicas | 1 | 2 | 2 (HPA 2→5) |
| API URL | localhost:30080 |
staging.autocom.io |
autocom.io |
| Service type | NodePort | ClusterIP + Ingress | ClusterIP + Ingress |
| TLS | None | Let's Encrypt staging | Let's Encrypt prod |
| APP_DEBUG | true | true | false |
| Images | Local (imagePullPolicy: Never) |
Registry | Registry |
Container Registry
Images are built by GitLab CI and pushed to registry.wexron.io:
Push to main → GitLab CI builds → registry.wexron.io/autocommerce/main/api:latest
→ registry.wexron.io/autocommerce/main/frontend:latest
→ registry.wexron.io/autocommerce/main/horizon:latest
Registry Access
K8s needs a pull secret to access the GitLab registry:
bash bin/vps-deploy/02-deploy-k3s-stack.sh <gitlab-token>
This creates a gitlab-registry docker-registry secret. All deployments reference it via imagePullSecrets.
Image Tagging
| Tag | Purpose |
|---|---|
latest |
Most recent build from main |
<commit-sha> |
Specific version for rollbacks |
Runtime Environment Injection
The frontend uses NEXT_PUBLIC_* variables which are normally baked at build time. AutoCom solves this with a runtime entrypoint script:
- Build time: Dockerfile uses placeholder strings (
__NEXT_PUBLIC_API_URL__) - Startup:
entrypoint.shreplaces placeholders with actual env vars viased - Result: One image works on any deployment — just set the env var
# In K8s deployment
env:
- name: NEXT_PUBLIC_API_URL
value: "https://api.autocom.io/api/v1" # Set per environment
No rebuild needed when changing the API URL.
Database: CloudNativePG
PostgreSQL runs via the CloudNativePG operator. The default overlay deploys a 3-instance cluster (1 primary + 2 streaming replicas).
One-time setup
# 1. Install the CNPG operator (cluster-wide)
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm install cnpg cnpg/cloudnative-pg \
--namespace cnpg-system --create-namespace --wait
# 2. Create the bootstrap secrets
# — re-use the same DB password your app already uses (autocom-secrets/DB_PASSWORD)
DB_PASS=$(kubectl -n autocom-k8s get secret autocom-secrets \
-o jsonpath='{.data.DB_PASSWORD}' | base64 -d)
kubectl -n autocom-k8s create secret generic autocom-db-app \
--from-literal=username=autocom \
--from-literal=password="$DB_PASS"
kubectl -n autocom-k8s create secret generic autocom-db-superuser \
--from-literal=username=postgres \
--from-literal=password="$DB_PASS"
# 3. Apply the overlay — the postgresql.cnpg.io/v1 Cluster comes up here
kubectl apply -k k8s/overlays/default
CNPG creates three services automatically:
| Service | Routes to | Use for |
|---|---|---|
autocom-db-rw |
Primary only | Writes (DB_HOST) |
autocom-db-ro |
Replicas only | Reads (DB_READ_HOST) |
autocom-db-r |
Any instance | Backups, ad-hoc queries |
Laravel's config/database.php is already wired for read/write splitting with sticky=true, so writes go to DB_HOST and reads go to DB_READ_HOST. After a write within a request, subsequent reads stick to the primary to avoid replication-lag surprises.
Network policy
The default-deny-ingress policy in the namespace requires an explicit allow rule for the operator. The overlay ships an allow-cnpg NetworkPolicy that:
- accepts
5432from in-namespace clients, - accepts
8000from thecnpg-systemnamespace (operator status API), - accepts
9187from themonitoringnamespace (metrics scrape), - allows replica streaming between instances,
- allows egress to
0.0.0.0/0:6443and:443(CNPG instances must reach the kube-apiserver, which on k3s runs on the host, not as a pod).
Storage
Each instance gets its own 20 GiB PVC on the cluster's default StorageClass (local-path on k3s). The namespace ResourceQuota must allow at least 60 GiB of requests.storage for the three CNPG PVCs plus your backup PVC.
External database alternative
If you'd rather use a managed Postgres (Neon, Supabase, RDS, Cloud SQL):
- Skip the operator install and the CNPG
Clusterin the overlay. - Set
DB_HOSTandDB_READ_HOSTin the ConfigMap to your managed endpoints (use the same endpoint twice if there's no read replica). - Make sure the cluster egress NetworkPolicy allows traffic to the managed DB host/port.
Deploy to Edge Device (Pi 5 / K3s)
No builds needed on the device — just pull from registry:
# 1. Install K3s
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik" sh -
# 2. Create namespace + registry secret
k3s kubectl create namespace autocom
k3s kubectl create secret docker-registry gitlab-registry \
--namespace=autocom \
--docker-server=registry.wexron.io \
--docker-username=deploy \
--docker-password=<gitlab-token>
# 3. Generate secrets
APP_KEY="base64:$(openssl rand -base64 32)"
DB_PASSWORD="autocom_$(openssl rand -hex 8)"
k3s kubectl create secret generic autocom-secrets -n autocom \
--from-literal=APP_KEY="$APP_KEY" \
--from-literal=DB_PASSWORD="$DB_PASSWORD" \
--from-literal=REDIS_PASSWORD="" \
--from-literal=AGENT_SHARED_SECRET="$(openssl rand -hex 32)"
# 4. Deploy (pulls images from registry)
k3s kubectl apply -k k8s/overlays/default
# 5. Run migrations
k3s kubectl exec -n autocom deployment/api -- php artisan migrate --force
k3s kubectl exec -n autocom deployment/api -- php artisan db:seed --force
k3s kubectl exec -n autocom deployment/api -- php artisan module:sync
Total deploy time: ~5 minutes (image pull + DB setup). Zero compilation on the device.
Secrets Management
Secrets are generated at deploy time, never stored in git:
| Secret | Contains | Generated by |
|---|---|---|
autocom-secrets |
APP_KEY, DB_PASSWORD, AGENT_SECRET | deploy.sh or manually |
passport-keys |
OAuth private/public keys | php artisan passport:keys |
gitlab-registry |
Docker registry credentials | registry-secret.sh |
autocom-db-app |
App role (autocom) for the CNPG cluster |
manually (matches autocom-secrets/DB_PASSWORD) |
autocom-db-superuser |
Postgres superuser for CNPG | manually (matches autocom-secrets/DB_PASSWORD) |
Environment Variables
See k8s/ENV_REFERENCE.md for the complete list of all environment variables per service.
Post-Deploy Commands
# Generate app key (first deploy)
kubectl exec -n autocom deployment/api -- php artisan key:generate --force
# Run migrations
kubectl exec -n autocom deployment/api -- php artisan migrate --force
# Generate Passport keys
kubectl exec -n autocom deployment/api -- php artisan passport:keys --force
kubectl exec -n autocom deployment/api -- php artisan passport:client --personal --name="AutoCom"
# Sync modules
kubectl exec -n autocom deployment/api -- php artisan module:sync
# Seed initial data
kubectl exec -n autocom deployment/api -- php artisan db:seed --force
# Optimize (production)
kubectl exec -n autocom deployment/api -- php artisan optimize
Monitoring
# Pod status
kubectl get pods -n autocom
# Resource usage
kubectl top pods -n autocom
# HPA status
kubectl get hpa -n autocom
# CloudNativePG cluster health
kubectl get cluster -n autocom
# API logs
kubectl logs -n autocom deployment/api --tail=50
# Octane worker status
kubectl exec -n autocom deployment/api -- php artisan octane:status