Skip to main content

Documentation

Deployment

Production deployment guide

Deployment

๐Ÿš€ Production Deployment Guide

Recommended path: deploy on Google Cloud Platform โ€” see GCP Deployment below. The cost model and pricing tiers this deployment target is designed around are documented in BUSINESS_PLAN_GCP.md. The generic Docker/systemd instructions further down remain valid for self-hosting on any VM or bare metal.


Two paths, matched to the tiers in BUSINESS_PLAN_GCP.md:

  • Cloud Run โ€” for Free/Starter/Pro tiers and most self-hosters. True scale-to-zero, no cluster to manage, cheapest option by a wide margin below sustained-load traffic.
  • GKE Autopilot โ€” for Team/Enterprise tiers, or once you need Qdrant/Redis/Postgres running as first-class stateful workloads alongside Stackhouse. Uses the existing Helm chart at deploy/helm/stackhouse/.

Both assume the stackhouse/Dockerfile in this repo, which already builds the UI and a slim Debian runtime image.

Option A: Cloud Run (launch / low-cost path)

1. One-time project setup

export PROJECT_ID=your-gcp-project
export REGION=us-central1
gcloud config set project $PROJECT_ID

gcloud services enable \
  run.googleapis.com \
  sqladmin.googleapis.com \
  redis.googleapis.com \
  artifactregistry.googleapis.com \
  secretmanager.googleapis.com \
  vpcaccess.googleapis.com

gcloud artifacts repositories create stackhouse \
  --repository-format=docker --location=$REGION

2. Provision managed dependencies

# Cloud SQL for PostgreSQL โ€” start with the smallest tier, resize later
gcloud sql instances create stackhouse-pg \
  --database-version=POSTGRES_16 \
  --tier=db-f1-micro \
  --region=$REGION \
  --storage-auto-increase

gcloud sql databases create stackhouse --instance=stackhouse-pg

# Memorystore (Redis) โ€” Basic tier is enough below the Team plan
gcloud redis instances create stackhouse-cache \
  --size=1 --region=$REGION --tier=basic

# Serverless VPC connector so Cloud Run can reach Memorystore's private IP
gcloud compute networks vpc-access connectors create stackhouse-connector \
  --region=$REGION --range=10.8.0.0/28

3. Store secrets

echo -n "$(openssl rand -hex 32)" | gcloud secrets create stackhouse-jwt-secret --data-file=-
echo -n "sk_live_..." | gcloud secrets create stackhouse-stripe-secret --data-file=-
echo -n "whsec_..." | gcloud secrets create stackhouse-stripe-webhook-secret --data-file=-

4. Build and push

cd stackhouse
gcloud builds submit --tag $REGION-docker.pkg.dev/$PROJECT_ID/stackhouse/stackhouse:latest

5. Deploy

gcloud run deploy stackhouse \
  --image=$REGION-docker.pkg.dev/$PROJECT_ID/stackhouse/stackhouse:latest \
  --region=$REGION \
  --platform=managed \
  --allow-unauthenticated \
  --port=8080 \
  --min-instances=0 \
  --max-instances=10 \
  --cpu=1 --memory=512Mi \
  --vpc-connector=stackhouse-connector \
  --add-cloudsql-instances=$PROJECT_ID:$REGION:stackhouse-pg \
  --set-env-vars="STACKHOUSE_HOST=0.0.0.0,STACKHOUSE_PORT=8080,STACKHOUSE_ENABLE_BILLING=true" \
  --set-env-vars="STACKHOUSE_URL=postgres://postgres:PASSWORD@/stackhouse?host=/cloudsql/$PROJECT_ID:$REGION:stackhouse-pg" \
  --set-secrets="STACKHOUSE_JWT_SECRET=stackhouse-jwt-secret:latest,STRIPE_SECRET_KEY=stackhouse-stripe-secret:latest,STACKHOUSE_BILLING_STRIPE_SIGNING_SECRET=stackhouse-stripe-webhook-secret:latest"

--min-instances=0 is what makes Free/Starter tenants near-free to host when idle โ€” see BUSINESS_PLAN_GCP.md ยง5โ€“6 for the cost model this relies on. Raise it to 1 for Pro-tier tenants that need to avoid cold starts.

Cost note: at list price this is $0.000024/vCPU-sec + $0.0000025/GiB-sec, with the first 180K vCPU-sec and 360K GiB-sec/month free โ€” a low-traffic tenant costs cents per month. Re-check current rates at cloud.google.com/run/pricing.

Option B: GKE Autopilot (Team / Enterprise / high-sustained-load)

The Helm chart in deploy/helm/stackhouse/ already ships Qdrant, PostgreSQL (Bitnami sub-chart), and Redis as dependencies โ€” Autopilot just removes node management.

gcloud container clusters create-auto stackhouse-cluster --region=$REGION

gcloud container clusters get-credentials stackhouse-cluster --region=$REGION

# Create the secrets the chart expects (see deploy/helm/stackhouse/values.yaml)
kubectl create secret generic stackhouse-secrets \
  --from-literal=database-url="postgres://stackhouse:PASSWORD@stackhouse-postgresql:5432/stackhouse" \
  --from-literal=jwt-secret="$(openssl rand -hex 32)" \
  --from-literal=redis-url="redis://:PASSWORD@stackhouse-redis-master:6379" \
  --from-literal=qdrant-url="http://stackhouse-qdrant:6333" \
  --from-literal=data-encryption-key="$(openssl rand -hex 32)"

helm dependency update deploy/helm/stackhouse
helm install stackhouse deploy/helm/stackhouse \
  --set stackhouse.image.repository=$REGION-docker.pkg.dev/$PROJECT_ID/stackhouse/stackhouse \
  --set stackhouse.image.tag=latest

Autopilot bills per-Pod CPU/memory request rather than per-node, so the chart's existing resources.requests in values.yaml directly determine cost โ€” tune them down for smaller Team-tier tenants rather than over-provisioning by default.

Choosing between them

Cloud RunGKE Autopilot
Best forFree, Starter, Pro, most self-hostersTeam, Enterprise, sustained high load
Scale-to-zeroYesNo (Autopilot still bills scheduled Pod requests)
Stateful sidecars (Qdrant/Redis/Postgres in-cluster)No โ€” use managed Cloud SQL/Memorystore insteadYes, via the Helm chart
Ops overheadNear zeroLow (no node management, but still a cluster)

Docker Deployment (generic / any host)

Stackhouse is a Postgres-backed server (StackhouseStore connects via the STACKHOUSE_URL connection string), so a real deployment needs a Postgres instance alongside it โ€” not just a local data volume.

docker-compose.yml:

version: '3.8'
services:
  postgres:
    image: postgres:16
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=stackhouse
    volumes:
      - stackhouse_pg:/var/lib/postgresql/data

  stackhouse:
    image: stackhouse:latest
    ports:
      - "3000:3000"
    volumes:
      - stackhouse_storage:/app/storage
    depends_on:
      - postgres
    environment:
      - RUST_LOG=info
      - STACKHOUSE_URL=postgres://postgres:postgres@postgres:5432/stackhouse
      - STACKHOUSE_JWT_SECRET=change-me
      - STACKHOUSE_STORAGE_PATH=/app/storage

volumes:
  stackhouse_pg:
  stackhouse_storage:
docker-compose up -d

Systemd Service

[Unit]
Description=Stackhouse Server
After=network.target

[Service]
Type=simple
User=stackhouse
WorkingDirectory=/opt/stackhouse
Environment=STACKHOUSE_URL=postgres://stackhouse:PASSWORD@localhost:5432/stackhouse
Environment=STACKHOUSE_JWT_SECRET=your-secret-key
Environment=STACKHOUSE_STORAGE_PATH=/var/lib/stackhouse/storage
ExecStart=/usr/local/bin/stackhouse serve \
  --host 0.0.0.0 \
  --port 3000
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Configuration

Stackhouse is configured entirely via CLI flags/environment variables โ€” there is no config.toml file. The serve subcommand (stackhouse serve) accepts:

FlagEnv varDefaultPurpose
-u, --url (global)STACKHOUSE_URLpostgres://postgres:postgres@localhost:5432/stackhousePostgres connection string
-p, --portSTACKHOUSE_PORT3000Port to bind
-h, --host*STACKHOUSE_HOST0.0.0.0Host to bind
-m, --memorySTACKHOUSE_MEMORYoffUse an in-memory database (testing only)
--jwt-secretSTACKHOUSE_JWT_SECRETnoneRequired for server mode
--storage-pathSTACKHOUSE_STORAGE_PATHnoneLocal path for file storage objects

* -h is normally reserved for --help, but clap here binds it to --host; use the long flag if that's ambiguous in your shell setup.

Reverse Proxy (Nginx)

location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Scaling

Vertical Scaling

  • More CPU cores
  • More memory
  • Faster SSD

Horizontal Scaling

  • Read replicas (see Replication)
  • Connection pooling
  • Load balancing

Next: Performance