Skip to main content
Deploy Piper to DigitalOcean Kubernetes with managed PostgreSQL and Redis.

Prerequisites

  • DigitalOcean account with billing enabled
  • doctl CLI installed and authenticated
  • kubectl installed
  • helm 3.14+ installed
  • A domain you control (for SSL)

Setup

1. Install and Authenticate doctl

# Install (macOS)
brew install doctl

# Authenticate with your API token
# Create token at: https://cloud.digitalocean.com/account/api/tokens
doctl auth init

2. Create Kubernetes Cluster

doctl kubernetes cluster create piper-cluster \
  --region nyc1 \
  --node-pool "name=default;size=s-2vcpu-4gb;count=3" \
  --version latest

# Save kubeconfig
doctl kubernetes cluster kubeconfig save piper-cluster

# Verify connection
kubectl get nodes

3. Create Managed PostgreSQL

doctl databases create piper-postgres \
  --engine pg \
  --version 16 \
  --size db-s-2vcpu-4gb \
  --region nyc1 \
  --num-nodes 1
Wait for the database to be online (~5 minutes):
doctl databases list
# Wait until Status shows "online"
Then enable the pgvector extension:
  1. Get the database ID and connection details:
# Get the database ID
doctl databases list

# Get connection details (use the ID from above)
doctl databases connection <database-id> --format Host,Port,User,Password
  1. Connect via psql (replace values from above):
PGPASSWORD=your-password psql -h your-host.db.ondigitalocean.com -p 25060 -U doadmin -d defaultdb
  1. Create the piper database:
CREATE DATABASE piper;
\q
  1. Reconnect to the piper database and enable pgvector:
PGPASSWORD=your-password psql -h your-host.db.ondigitalocean.com -p 25060 -U doadmin -d piper
CREATE EXTENSION IF NOT EXISTS vector;
\q

4. Create Managed Valkey (Redis-compatible)

doctl databases create piper-valkey \
  --engine valkey \
  --version 8 \
  --size db-s-1vcpu-2gb \
  --region nyc1 \
  --num-nodes 1

5. Install Nginx Ingress Controller

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/do-loadbalancer-name"="piper-lb"
Get the load balancer IP (may take a few minutes):
kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

6. Install cert-manager (for Let’s Encrypt SSL)

helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true
Create a ClusterIssuer for Let’s Encrypt:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]  # CHANGE THIS
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx
EOF

7. Configure DNS

Point your domain to the load balancer IP:
  1. Get the load balancer IP from step 5
  2. In your DNS provider, create an A record:
    • Host: piper (or your subdomain)
    • Value: <LOAD_BALANCER_IP>
    • TTL: 300

Deploy Piper

1. Create Namespace and Secrets

kubectl create namespace piper
Generate application secrets (save the admin secret - you’ll need it later):
export JWT_SECRET=$(openssl rand -hex 32)
export ADMIN_API_SECRET=$(openssl rand -hex 32)
echo "Admin API Secret: $ADMIN_API_SECRET"  # Save this!
Get your database passwords from the DigitalOcean Console:
  • PostgreSQL: Databases → piper-postgres → Connection Details → Password
  • Valkey: Databases → piper-valkey → Connection Details → Password
# Application secrets
kubectl create secret generic piper-secrets \
  --namespace piper \
  --from-literal=jwt-secret="$JWT_SECRET" \
  --from-literal=admin-api-secret="$ADMIN_API_SECRET"

# Database credentials
kubectl create secret generic piper-db-credentials \
  --namespace piper \
  --from-literal=password='YOUR_POSTGRES_PASSWORD'

kubectl create secret generic piper-valkey-credentials \
  --namespace piper \
  --from-literal=password='YOUR_VALKEY_PASSWORD'
Save the ADMIN_API_SECRET - you’ll need it to create your first organization and user via the Admin API.

2. Create Image Pull Secret

kubectl create secret docker-registry ghcr-pull-secret \
  --namespace piper \
  --docker-server=ghcr.io \
  --docker-username=YOUR_GITHUB_USERNAME \
  --docker-password=YOUR_GITHUB_PAT

3. Configure Values

Copy the DigitalOcean values template:
cp helm/piper/values-digitalocean.yaml helm/piper/values-my-deployment.yaml
Edit values-my-deployment.yaml and fill in:
externalDatabase:
  host: "your-db-host.db.ondigitalocean.com"  # From DO Console
  existingSecret: piper-db-credentials

externalRedis:
  host: "your-redis-host.db.ondigitalocean.com"  # From DO Console
  existingSecret: piper-valkey-credentials

ingress:
  host: "piper.yourcompany.com"
  tls:
    - secretName: piper-tls
      hosts:
        - "piper.yourcompany.com"

4. Deploy with Helm

# Fetch chart dependencies (first time only)
helm dependency build ./helm/piper

# Deploy (secrets are referenced from the piper-secrets created earlier)
helm upgrade --install piper ./helm/piper \
  --namespace piper \
  -f helm/piper/values-my-deployment.yaml \
  --set config.existingSecret=piper-secrets

5. Verify Deployment

# Check pods are running
kubectl get pods -n piper

# Check services
kubectl get svc -n piper

# Check ingress and certificate
kubectl get ingress -n piper
kubectl get certificate -n piper

# View API logs
kubectl logs -n piper -l app.kubernetes.io/component=api -f

6. Test the Deployment

# Health check
curl https://piper.yourcompany.com/api/health

# Should return: {"status": "healthy"}

Updating

To deploy a new version:
# Update the image tag in your values file, then:
helm upgrade piper ./helm/piper \
  --namespace piper \
  -f helm/piper/values-my-deployment.yaml
Or update inline:
helm upgrade piper ./helm/piper \
  --namespace piper \
  -f helm/piper/values-my-deployment.yaml \
  --set backend.image.tag="0.2.0" \
  --set frontend.image.tag="0.2.0"

Scaling

Manual Scaling

kubectl scale deployment piper-api -n piper --replicas=4
kubectl scale deployment piper-worker -n piper --replicas=4

Enable Autoscaling

Update your values file:
api:
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilizationPercentage: 80

worker:
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 20
    targetCPUUtilizationPercentage: 70
Then run helm upgrade.

Troubleshooting

Pods not starting

# Check pod status
kubectl describe pod -n piper <pod-name>

# Check events
kubectl get events -n piper --sort-by=.metadata.creationTimestamp

Database connection issues

# Test PostgreSQL connectivity
kubectl run -it --rm debug --image=postgres:16 --restart=Never -n piper -- \
  psql "postgresql://doadmin:PASSWORD@HOST:25060/piper?sslmode=require" -c "SELECT 1"

Certificate not issuing

# Check certificate status
kubectl describe certificate piper-tls -n piper

# Check cert-manager logs
kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-manager -f

View all logs

# API
kubectl logs -n piper -l app.kubernetes.io/component=api -f

# Worker
kubectl logs -n piper -l app.kubernetes.io/component=worker -f

# Frontend
kubectl logs -n piper -l app.kubernetes.io/component=frontend -f

Next Steps

Once your deployment is running, create your first organization and admin user:

Bootstrap Your Installation

Create the first organization and admin user using the Admin API

Cleanup

To remove the deployment:
# Remove Piper
helm uninstall piper -n piper
kubectl delete namespace piper

# Remove infrastructure (optional)
doctl kubernetes cluster delete piper-cluster
doctl databases delete piper-postgres
doctl databases delete piper-valkey