Deploying firmflow. to DigitalOcean Kubernetes (DOKS)¶
Domain: firmflow.com.ng
Target: DigitalOcean Kubernetes Service (DOKS)
TLS: Let's Encrypt via cert-manager
Registry: DigitalOcean Container Registry (DOCR)
CI/CD: GitHub Actions → DOCR (.github/workflows/deploy-docr.yml)
CI/CD Pipeline (GitHub Actions)¶
The repository includes a GitHub Actions workflow that automatically builds and pushes a new Docker image to DOCR on every push to main.
Required GitHub Secrets¶
Go to your repository → Settings → Secrets and variables → Actions → New repository secret and add:
| Secret Name | Value | Where to get it |
|---|---|---|
DO_ACCESS_TOKEN |
Your DigitalOcean Personal Access Token | cloud.digitalocean.com/account/api/tokens |
DO_REGISTRY_NAME |
Your DOCR name (e.g. firmflow-registry) |
Created in Step 2 below |
What the workflow does¶
Push to main
│
▼
Job 1: Lint & Typecheck (npm run typecheck + lint)
│ passes
▼
Job 2: Build Docker image (multi-stage, node:20-alpine)
│
├── Tags: latest, sha-<commit>, main, semver (if tagged)
├── Caches layers in GitHub Actions cache (faster builds)
└── Pushes to registry.digitalocean.com/<registry>/firmflow-app
│
▼
GitHub Actions Summary shows image tag + Helm deploy command
To also auto-deploy to your DOKS cluster after each push, uncomment the deploy job in .github/workflows/deploy-docr.yml.
Ensure the following tools are installed and authenticated on your local machine:
# Install doctl (DigitalOcean CLI)
brew install doctl # macOS
# or: snap install doctl # Ubuntu
# Authenticate with your DigitalOcean API token
doctl auth init
# Install kubectl
brew install kubectl
# Install Helm 3
brew install helm
# Verify all tools
doctl version && kubectl version --client && helm version
Step 1: Create the DOKS Cluster¶
# List available regions (choose one close to Nigeria — e.g. Amsterdam 'ams3' or Frankfurt 'fra1')
doctl kubernetes options regions
# Create the cluster (takes ~5 minutes)
doctl kubernetes cluster create firmflow-prod \
--region fra1 \
--node-pool "name=worker-pool;size=s-4vcpu-8gb;count=2" \
--version latest
# Configure kubectl to use the new cluster
doctl kubernetes cluster kubeconfig save firmflow-prod
# Verify cluster access
kubectl get nodes
Recommended Node Size
s-4vcpu-8gb (4 vCPU, 8 GB RAM) handles up to 30 concurrent users. Scale to s-8vcpu-16gb for 50+ users.
Step 2: Set Up DigitalOcean Container Registry (DOCR)¶
# Create a private container registry
doctl registry create firmflow-registry --region fra1
# Authenticate Docker with DOCR
doctl registry login
# Your registry URL will be:
# registry.digitalocean.com/firmflow-registry
Build & Push the Docker Image¶
# Build the production image
docker build -t registry.digitalocean.com/firmflow-registry/firmflow-app:v1.2.0 .
docker tag registry.digitalocean.com/firmflow-registry/firmflow-app:v1.2.0 \
registry.digitalocean.com/firmflow-registry/firmflow-app:latest
# Push to DOCR
docker push registry.digitalocean.com/firmflow-registry/firmflow-app:v1.2.0
docker push registry.digitalocean.com/firmflow-registry/firmflow-app:latest
Integrate Registry with Cluster¶
# Allow the cluster to pull from DOCR automatically
doctl registry kubernetes-manifest | kubectl apply -f -
Step 3: Configure DNS¶
In your domain registrar for firmflow.com.ng, point the nameservers to DigitalOcean:
Then manage DNS records in DigitalOcean:
# After the Ingress is created (Step 7), get the Load Balancer IP:
kubectl get svc -n ingress-nginx
# Add DNS A records pointing to that IP:
doctl compute domain records create firmflow.com.ng \
--record-type A \
--record-name @ \
--record-data <LOAD_BALANCER_IP> \
--record-ttl 3600
doctl compute domain records create firmflow.com.ng \
--record-type A \
--record-name www \
--record-data <LOAD_BALANCER_IP> \
--record-ttl 3600
DNS Propagation
DNS changes can take up to 48 hours, but typically resolve within 15–30 minutes with DigitalOcean nameservers. Verify with: nslookup firmflow.com.ng
Step 4: Install Nginx Ingress Controller¶
# Add the ingress-nginx Helm repo
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
# Install — DigitalOcean will auto-provision a Load Balancer
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.service.type=LoadBalancer \
--set controller.service.annotations."service\.beta\.kubernetes\.io/do-loadbalancer-name"=firmflow-lb
# Wait for the Load Balancer IP to be assigned (1-2 minutes)
kubectl get svc -n ingress-nginx --watch
Step 5: Install cert-manager (Let's Encrypt TLS)¶
# Add the cert-manager Helm repo
helm repo add jetstack https://charts.jetstack.io
helm repo update
# Install cert-manager with CRDs
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
# Verify all cert-manager pods are running
kubectl get pods -n cert-manager
Create a ClusterIssuer for Let's Encrypt¶
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@firmflow.com.ng
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
Step 6: Create Kubernetes Secrets¶
Generate all required secrets before deploying the application:
# Generate secure random values
AUTH_SECRET=$(openssl rand -base64 32)
LICENSE_SECRET=$(openssl rand -base64 32)
FIELD_KEY=$(openssl rand -hex 32)
STORAGE_KEY=$(openssl rand -hex 32)
# Create the main application secret
kubectl create secret generic firmflow-secrets \
--namespace default \
--from-literal=AUTH_SECRET="$AUTH_SECRET" \
--from-literal=LICENSE_SECRET="$LICENSE_SECRET" \
--from-literal=FIELD_ENCRYPTION_KEY="$FIELD_KEY" \
--from-literal=STORAGE_ENCRYPTION_KEY="$STORAGE_KEY" \
--from-literal=DATABASE_URL="postgresql://firmflow_user:YOUR_DB_PASSWORD@your-db-host:5432/firmflow" \
--from-literal=NEXTAUTH_URL="https://firmflow.com.ng" \
--from-literal=NEXT_PUBLIC_APP_URL="https://firmflow.com.ng" \
--from-literal=GOOGLE_GENAI_API_KEY="YOUR_GEMINI_API_KEY" \
--from-literal=RESEND_API_KEY="YOUR_RESEND_API_KEY" \
--from-literal=PAYSTACK_SECRET_KEY="YOUR_PAYSTACK_SECRET_KEY"
Secret Storage
Save all generated values (especially AUTH_SECRET, LICENSE_SECRET) in a secure password manager immediately. They cannot be recovered from Kubernetes once created.
Optional: Managed Database (Recommended)¶
Use DigitalOcean Managed PostgreSQL instead of in-cluster:
# Create a managed PostgreSQL cluster
doctl databases create firmflow-db \
--engine pg \
--region fra1 \
--size db-s-1vcpu-1gb \
--num-nodes 1
# Get the connection string
doctl databases connection firmflow-db --format URI
Step 7: Deploy firmflow. via Helm¶
Create a production values override file:
cat > values.do-prod.yaml << 'EOF'
replicaCount: 2
image:
repository: registry.digitalocean.com/firmflow-registry/firmflow-app
pullPolicy: Always
tag: "v1.2.0"
imagePullSecrets:
- name: registry-firmflow-registry
service:
type: ClusterIP
port: 3000
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
nginx.ingress.kubernetes.io/limit-connections: "20"
nginx.ingress.kubernetes.io/limit-rps: "10"
hosts:
- host: firmflow.com.ng
paths:
- path: /
pathType: Prefix
- host: www.firmflow.com.ng
paths:
- path: /
pathType: Prefix
tls:
- secretName: firmflow-tls-cert
hosts:
- firmflow.com.ng
- www.firmflow.com.ng
resources:
limits:
cpu: 2000m
memory: 4Gi
requests:
cpu: 500m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 70
config:
nextAuthUrl: "https://firmflow.com.ng"
storageDriver: "local"
storageLocalPath: "/data/uploads"
postgresql:
enabled: false # Using DigitalOcean Managed PostgreSQL
persistence:
enabled: true
size: 100Gi
accessMode: ReadWriteOnce
storageClass: "do-block-storage"
EOF
Deploy¶
# Deploy (or upgrade if already installed)
helm upgrade --install firmflow ./kubernetes/firmflow \
--namespace default \
--values kubernetes/firmflow/values.yaml \
--values values.do-prod.yaml \
--set env.secretName=firmflow-secrets \
--wait \
--timeout 10m
# Watch the rollout
kubectl rollout status deployment/firmflow
Step 8: Run Database Migrations¶
# Run Prisma migrations on the live cluster
kubectl exec -it deployment/firmflow -- npx prisma migrate deploy
# Seed the database (creates super admin)
kubectl exec -it deployment/firmflow -- npx tsx prisma/seed.ts
Step 9: Verify the Deployment¶
# Check all pods are running
kubectl get pods
# Check ingress and TLS certificate
kubectl get ingress
kubectl describe certificate firmflow-tls-cert
# Check application logs
kubectl logs -f deployment/firmflow --tail=50
# Test the health endpoint
curl https://firmflow.com.ng/api/health
A successful health check returns:
Step 10: Post-Deployment Checklist¶
- [ ] Login at
https://firmflow.com.ng/loginwith Super Admin credentials - [ ] Change all default passwords immediately
- [ ] Enable MFA on the Super Admin account
- [ ] Verify document upload and AI analysis works end-to-end
- [ ] Set up automated DigitalOcean database backups (Daily snapshots)
- [ ] Configure Resend email with
firmflow.com.ngdomain verification - [ ] Verify Paystack webhook URL:
https://firmflow.com.ng/api/billing/webhook - [ ] Apply the Kubernetes NetworkPolicy:
kubectl apply -f kubernetes/firmflow/templates/network-policy.yaml
Upgrading the Application¶
# Build and push a new image version
docker build -t registry.digitalocean.com/firmflow-registry/firmflow-app:v1.3.0 .
docker push registry.digitalocean.com/firmflow-registry/firmflow-app:v1.3.0
# Rolling upgrade with zero downtime
helm upgrade firmflow ./kubernetes/firmflow \
--values kubernetes/firmflow/values.yaml \
--values values.do-prod.yaml \
--set image.tag=v1.3.0 \
--wait
# Run any new migrations
kubectl exec -it deployment/firmflow -- npx prisma migrate deploy
Monitoring & Observability¶
# View real-time application logs
kubectl logs -f deployment/firmflow
# Check resource usage
kubectl top pods
kubectl top nodes
# Access the DigitalOcean Kubernetes dashboard
doctl kubernetes cluster get firmflow-prod
Estimated Monthly Cost (DigitalOcean)¶
| Resource | Size | Est. Cost |
|---|---|---|
| DOKS Worker Nodes (×2) | s-4vcpu-8gb |
~$96/mo |
| Managed PostgreSQL | db-s-1vcpu-1gb |
~$15/mo |
| Load Balancer | Standard | ~$12/mo |
| Container Registry | Starter | ~$5/mo |
| Block Storage (100Gi) | DO Volumes | ~$10/mo |
| Total | ~$138/mo |
Nigerian Market Note
DigitalOcean's fra1 (Frankfurt) region provides the best latency for Nigerian users (~60-80ms). The blr1 (Bangalore) region is not recommended. Consider the Singapore (sgp1) datacenter as an alternative.
helm upgrade --install firmflow ./kubernetes/firmflow -f values.do-prod.yaml