- OCI tenancy with a compartment for SimaOps
- OCI CLI authenticated (
oci setup config) - Node.js 20+, pnpm 9+
- Helm 3.x
- GitHub repo secrets configured (see below)
Sign up at https://signup.oraclecloud.com (Always Free tier sufficient for staging).
mkdir -p ~/.oci
oci setup config
# Follow prompts: tenancy OCID, user OCID, region (ap-singapore-1)
# It will generate ~/.oci/oci_api_key.pem and add the public key to your userOr generate manually:
openssl genrsa -out ~/.oci/oci_api_key.pem 2048
chmod 600 ~/.oci/oci_api_key.pem
openssl rsa -pubout -in ~/.oci/oci_api_key.pem -out ~/.oci/oci_api_key_public.pemThen in OCI Console: User → API Keys → Add Public Key → paste the public key contents. Note the fingerprint that's displayed.
OCI Console → Identity & Security → Compartments → Create Compartment (e.g., simaops). Note the compartment OCID.
export OCI_TENANCY_OCID="ocid1.tenancy.oc1..aaaa..."
export OCI_USER_OCID="ocid1.user.oc1..aaaa..."
export OCI_FINGERPRINT="aa:bb:cc:dd:..."
export OCI_PRIVATE_KEY="$(cat ~/.oci/oci_api_key.pem)"
export OCI_REGION="ap-singapore-1"
export OCI_COMPARTMENT_OCID="ocid1.compartment.oc1..aaaa..."make stack-up # Start TiDB, MinIO, NATS, Keycloak, Jaeger
make db-migrate # Run schema migrations
make gen # Generate proto stubs
cd apps/api && go run ./cmd/api # Start Go API on :8080
cd apps/web && pnpm dev # Start SvelteKit on :5173
cd apps/ai-worker && python main.py # Start AI worker on :8081
cd apps/outbox-publisher && go run ./cmd/publisher # Start outbox publishercd infra/sst
pnpm install
npx sst deploy --stage stagingThis provisions:
- VCN
10.0.0.0/16+ public subnet + Internet Gateway + Route Table + Security List - OKE Basic Cluster (free control plane, Kubernetes v1.30.x)
- Node pool: 2×
VM.Standard.E4.Flex(AMD x86_64), 2 OCPU / 16 GB each, BASELINE_1_8 burstable
After deploy completes, populate kubeconfig:
oci ce cluster create-kubeconfig \
--cluster-id $(npx sst output clusterOcid --stage staging) \
--file ~/.kube/config \
--region ap-singapore-1 \
--token-version 2.0.0 \
--kube-endpoint PUBLIC_ENDPOINT
kubectl get nodes # should show 2 ready nodes# Add helm repos
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add pingcap https://charts.pingcap.org/
helm repo add nats https://nats-io.github.io/k8s/helm/charts
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
# Platform namespace
kubectl create namespace platform
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx -n platform
helm upgrade --install cert-manager jetstack/cert-manager -n platform --set installCRDs=true
# Keycloak — Postgres-backed (persistent realm), imported on first boot.
kubectl -n platform create secret generic keycloak-db \
--from-literal=username=keycloak --from-literal=password="$(openssl rand -base64 18)"
kubectl -n platform create secret generic keycloak-admin \
--from-literal=username=admin --from-literal=password="$(openssl rand -base64 18)"
# Realm import source (secrets via ${ENV} placeholders are substituted before this step).
kubectl -n platform create configmap simaops-realm --from-file=simaops-realm.json=deploy/keycloak/simaops-realm.json
kubectl apply -f deploy/keycloak/keycloak-postgres.yaml
kubectl apply -f deploy/keycloak/keycloak.yaml
helm upgrade --install tidb-operator pingcap/tidb-operator -n platform
helm upgrade --install minio bitnami/minio -n platform
helm upgrade --install nats nats/nats -n platform --set jetstream.enabled=true
# Observability namespace
kubectl create namespace observability
helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack -n observability \
-f infra/values/kube-prometheus-stack.yaml # PVC-backed storage + 30d retention
helm upgrade --install loki grafana/loki-stack -n observability
helm upgrade --install tempo grafana/tempo -n observability
# App namespace
for app in api web ai-worker outbox-publisher; do
helm upgrade --install simaops-$app deploy/helm/simaops-$app \
-n simaops --create-namespace
donekubectl get svc -n platform ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'URLs (using sslip.io):
https://app.<lb-ip>.sslip.io→ SvelteKit frontendhttps://api.<lb-ip>.sslip.io→ Go APIhttps://auth.<lb-ip>.sslip.io→ Keycloakhttps://grafana.<lb-ip>.sslip.io→ Grafana
| Workflow | Trigger | Action |
|---|---|---|
build.yaml |
Push to main / PR | Build + push images to ghcr.io |
deploy-staging.yaml |
After build succeeds on main | Deploy to OKE staging |
deploy-production.yaml |
Release tag published | Deploy to OKE production (manual approval) |
The deploy workflows authenticate to OCI via the standard OCI_CLI_*
environment variables consumed by the oracle-actions/* actions. Set these
in Settings → Secrets and variables → Actions.
| Secret | Value | Used by |
|---|---|---|
OCI_CLI_USER |
Deploy user OCID | both |
OCI_CLI_TENANCY |
Tenancy OCID | both |
OCI_CLI_FINGERPRINT |
API signing-key fingerprint | both |
OCI_CLI_KEY_CONTENT |
Contents of the staging deploy key PEM | staging |
OCI_CLI_KEY_CONTENT_PROD |
Contents of the production deploy key PEM | production |
OCI_CLI_REGION |
ap-singapore-1 |
both |
OCI_CLUSTER_OCID |
Staging OKE cluster OCID | staging |
OCI_CLUSTER_OCID_PROD |
Production OKE cluster OCID | production |
See oci-ci-deploy-user.md for the one-time setup
that creates a least-privilege CI deploy user, the IAM policy it needs, and
the exact gh secret set commands to populate the table above.
Currently using sslip.io (free wildcard DNS): <lb-ip>.sslip.io resolves to <lb-ip>.
When a real domain is available, update infra/values/staging.yaml:
dns:
strategy: oci_dns
domain: simaops.example.comAnd update Helm releases to use the real domain in ingress hostnames.