--- title: Kubernetes description: Deploy Documenso on Kubernetes for production environments requiring high availability, auto-scaling, and enterprise-grade infrastructure. --- import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Callout } from 'fumadocs-ui/components/callout'; ## Prerequisites Before starting, ensure you have: - A Kubernetes cluster (1.24+) running and accessible - `kubectl` configured to communicate with your cluster - A PostgreSQL 14+ database (in-cluster or external) - SMTP credentials for sending emails - A domain name with DNS configured - An Ingress controller installed (nginx-ingress, traefik, etc.) Verify your cluster connection: ```bash kubectl cluster-info kubectl get nodes ``` ## Architecture Overview A typical Documenso Kubernetes deployment consists of: ```mermaid flowchart TD A["Ingress (TLS termination)"] --> B["Service (ClusterIP)"] B --> C1["Pod (app)"] B --> C2["Pod (app)"] B --> C3["Pod (app)"] C1 --> D["PostgreSQL (external or in-cluster)"] C2 --> D C3 --> D ``` ## Namespace Setup Create a dedicated namespace for Documenso: ```yaml # namespace.yaml apiVersion: v1 kind: Namespace metadata: name: documenso labels: app.kubernetes.io/name: documenso ``` Apply the namespace: ```bash kubectl apply -f namespace.yaml ``` ## Secrets Configuration Store sensitive configuration in Kubernetes Secrets. ### Generate Secrets First, generate the required secret values: ```bash # Generate NEXTAUTH_SECRET openssl rand -base64 32 # Generate encryption keys (minimum 32 characters each) openssl rand -base64 32 openssl rand -base64 32 ``` ### Create the Secret ```yaml # secret.yaml apiVersion: v1 kind: Secret metadata: name: documenso-secrets namespace: documenso labels: app.kubernetes.io/name: documenso type: Opaque stringData: NEXTAUTH_SECRET: 'your-generated-secret-here' NEXT_PRIVATE_ENCRYPTION_KEY: 'your-encryption-key-min-32-chars' NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: 'your-secondary-key-min-32-chars' NEXT_PRIVATE_DATABASE_URL: 'postgresql://user:password@postgres-host:5432/documenso' NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://user:password@postgres-host:5432/documenso' NEXT_PRIVATE_SMTP_USERNAME: 'your-smtp-username' NEXT_PRIVATE_SMTP_PASSWORD: 'your-smtp-password' NEXT_PRIVATE_SIGNING_PASSPHRASE: 'your-certificate-passphrase' ``` Never commit Secret manifests with real values to version control. Use a secrets management tool like Sealed Secrets, External Secrets Operator, or your cloud provider's secret manager. Apply the secret: ```bash kubectl apply -f secret.yaml ``` ## ConfigMap Store non-sensitive configuration in a ConfigMap: ```yaml # configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: documenso-config namespace: documenso labels: app.kubernetes.io/name: documenso data: NEXT_PUBLIC_WEBAPP_URL: 'https://sign.example.com' NEXT_PRIVATE_SMTP_TRANSPORT: 'smtp-auth' NEXT_PRIVATE_SMTP_HOST: 'smtp.example.com' NEXT_PRIVATE_SMTP_PORT: '587' NEXT_PRIVATE_SMTP_FROM_NAME: 'Documenso' NEXT_PRIVATE_SMTP_FROM_ADDRESS: 'noreply@example.com' NEXT_PRIVATE_INTERNAL_WEBAPP_URL: 'http://localhost:3000' NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: '/opt/documenso/cert.p12' NEXT_PUBLIC_UPLOAD_TRANSPORT: 'database' ``` Apply the ConfigMap: ```bash kubectl apply -f configmap.yaml ``` ## Deployment Create the Documenso Deployment: ```yaml # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web spec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web spec: securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 containers: - name: documenso image: documenso/documenso:latest imagePullPolicy: Always ports: - name: http containerPort: 3000 protocol: TCP envFrom: - configMapRef: name: documenso-config - secretRef: name: documenso-secrets resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1000m memory: 1Gi livenessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 startupProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 30 volumeMounts: - name: signing-cert mountPath: /opt/documenso/cert.p12 subPath: cert.p12 readOnly: true volumes: - name: signing-cert secret: secretName: documenso-signing-cert items: - key: cert.p12 path: cert.p12 ``` Pin to a specific image tag (e.g., `documenso/documenso:`) in production instead of `latest` to ensure predictable deployments. ### Signing Certificate Secret Create a secret for the signing certificate: ```bash kubectl create secret generic documenso-signing-cert \ --namespace documenso \ --from-file=cert.p12=/path/to/your/cert.p12 ``` Apply the deployment: ```bash kubectl apply -f deployment.yaml ``` ## Service Expose the Deployment with a Service: ```yaml # service.yaml apiVersion: v1 kind: Service metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web spec: type: ClusterIP ports: - name: http port: 80 targetPort: http protocol: TCP selector: app.kubernetes.io/name: documenso app.kubernetes.io/component: web ``` Apply the service: ```bash kubectl apply -f service.yaml ``` ## Ingress Configuration ### nginx-ingress ```yaml # ingress-nginx.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso annotations: nginx.ingress.kubernetes.io/proxy-body-size: '50m' nginx.ingress.kubernetes.io/proxy-read-timeout: '300' nginx.ingress.kubernetes.io/proxy-send-timeout: '300' cert-manager.io/cluster-issuer: 'letsencrypt-prod' spec: ingressClassName: nginx tls: - hosts: - sign.example.com secretName: documenso-tls rules: - host: sign.example.com http: paths: - path: / pathType: Prefix backend: service: name: documenso port: name: http ``` ### Traefik ```yaml # ingress-traefik.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.tls: 'true' cert-manager.io/cluster-issuer: 'letsencrypt-prod' spec: ingressClassName: traefik tls: - hosts: - sign.example.com secretName: documenso-tls rules: - host: sign.example.com http: paths: - path: / pathType: Prefix backend: service: name: documenso port: name: http ``` Apply your chosen ingress: ```bash kubectl apply -f ingress-nginx.yaml # or kubectl apply -f ingress-traefik.yaml ``` ## Database Options ### External Database (Recommended) For production, use a managed PostgreSQL service: - AWS RDS for PostgreSQL - Google Cloud SQL - Azure Database for PostgreSQL - DigitalOcean Managed Databases Update `NEXT_PRIVATE_DATABASE_URL` in your Secret with the connection string from your provider. ### In-Cluster PostgreSQL For testing or development, deploy PostgreSQL in the cluster: ```yaml # postgres.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-pvc namespace: documenso spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: v1 kind: Secret metadata: name: postgres-secrets namespace: documenso type: Opaque stringData: POSTGRES_USER: documenso POSTGRES_PASSWORD: your-secure-password POSTGRES_DB: documenso --- apiVersion: apps/v1 kind: Deployment metadata: name: postgres namespace: documenso spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: postgres template: metadata: labels: app.kubernetes.io/name: postgres spec: containers: - name: postgres image: postgres:15-alpine ports: - containerPort: 5432 envFrom: - secretRef: name: postgres-secrets volumeMounts: - name: postgres-data mountPath: /var/lib/postgresql/data resources: requests: cpu: 250m memory: 256Mi limits: cpu: 1000m memory: 1Gi volumes: - name: postgres-data persistentVolumeClaim: claimName: postgres-pvc --- apiVersion: v1 kind: Service metadata: name: postgres namespace: documenso spec: type: ClusterIP ports: - port: 5432 targetPort: 5432 selector: app.kubernetes.io/name: postgres ``` In-cluster PostgreSQL is not recommended for production. It lacks high availability, automated backups, and point-in-time recovery that managed services provide. Update your Documenso secret to use the in-cluster database: ```yaml NEXT_PRIVATE_DATABASE_URL: 'postgresql://documenso:your-secure-password@postgres.documenso.svc.cluster.local:5432/documenso' NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://documenso:your-secure-password@postgres.documenso.svc.cluster.local:5432/documenso' ``` ## Persistent Storage Documenso stores documents in the database by default. For high-volume deployments, configure S3-compatible storage. ### S3 Configuration Add these to your ConfigMap and Secret: ```yaml # In configmap.yaml, add: data: NEXT_PUBLIC_UPLOAD_TRANSPORT: 's3' # In secret.yaml, add: stringData: NEXT_PRIVATE_UPLOAD_ENDPOINT: 'https://s3.amazonaws.com' NEXT_PRIVATE_UPLOAD_REGION: 'us-east-1' NEXT_PRIVATE_UPLOAD_BUCKET: 'your-documenso-bucket' NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID: 'your-access-key' NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY: 'your-secret-key' ``` See [Storage Configuration](/docs/self-hosting/configuration/storage) for detailed S3 setup instructions. ## Scaling ### Horizontal Pod Autoscaler (HPA) Scale Documenso based on CPU and memory utilization: ```yaml # hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: documenso minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 10 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max ``` Apply the HPA: ```bash kubectl apply -f hpa.yaml ``` ### Pod Disruption Budget Ensure availability during cluster maintenance: ```yaml # pdb.yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso spec: minAvailable: 1 selector: matchLabels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web ``` Apply the PDB: ```bash kubectl apply -f pdb.yaml ``` ## Troubleshooting Check pod status: ```bash # Get pod status kubectl get pods -n documenso # Describe pod kubectl describe pod -n documenso ``` Common causes: - ImagePullBackOff (check image name and registry access) - CrashLoopBackOff (check logs for application errors) - Pending (check resource requests and node capacity) Verify the database is accessible from the cluster: ```bash kubectl run postgres-test --rm -it --image=postgres:15-alpine -n documenso -- psql "postgresql://user:password@host:5432/database" -c "SELECT 1" ``` Check Ingress status: ```bash # Get Ingress status kubectl get ingress -n documenso # Describe Ingress kubectl describe ingress documenso -n documenso ``` Verify the Ingress controller is running: ```bash # Get Ingress controller status kubectl get pods -n ingress-nginx # Get Traefik controller status kubectl get pods -n traefik ``` Check the signing certificate secret: ```bash # Get signing certificate secret kubectl get secret documenso-signing-cert -n documenso -o yaml ``` Verify the certificate is mounted correctly: ```bash # Execute command in pod kubectl exec -it -n documenso -- ls -la /opt/documenso/ ``` Test the health endpoint: ```bash # Port forward kubectl port-forward svc/documenso 3000:80 -n documenso # Test health endpoint curl http://localhost:3000/api/health ``` If a deployment fails, rollback: ```bash # View history kubectl rollout history deployment/documenso -n documenso # Rollback to previous version kubectl rollout undo deployment/documenso -n documenso # Rollback to a specific revision kubectl rollout undo deployment/documenso -n documenso --to-revision=2 ``` ## Complete Example Here's a combined manifest for reference: ```yaml # documenso-complete.yaml --- apiVersion: v1 kind: Namespace metadata: name: documenso labels: app.kubernetes.io/name: documenso --- apiVersion: v1 kind: ConfigMap metadata: name: documenso-config namespace: documenso data: NEXT_PUBLIC_WEBAPP_URL: 'https://sign.example.com' NEXT_PRIVATE_SMTP_TRANSPORT: 'smtp-auth' NEXT_PRIVATE_SMTP_HOST: 'smtp.example.com' NEXT_PRIVATE_SMTP_PORT: '587' NEXT_PRIVATE_SMTP_FROM_NAME: 'Documenso' NEXT_PRIVATE_SMTP_FROM_ADDRESS: 'noreply@example.com' NEXT_PRIVATE_INTERNAL_WEBAPP_URL: 'http://localhost:3000' NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: '/opt/documenso/cert.p12' NEXT_PUBLIC_UPLOAD_TRANSPORT: 'database' --- apiVersion: v1 kind: Secret metadata: name: documenso-secrets namespace: documenso type: Opaque stringData: NEXTAUTH_SECRET: 'REPLACE_WITH_GENERATED_SECRET' NEXT_PRIVATE_ENCRYPTION_KEY: 'REPLACE_WITH_32_CHAR_KEY' NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: 'REPLACE_WITH_32_CHAR_KEY' NEXT_PRIVATE_DATABASE_URL: 'postgresql://user:password@host:5432/documenso' NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://user:password@host:5432/documenso' NEXT_PRIVATE_SMTP_USERNAME: 'smtp-user' NEXT_PRIVATE_SMTP_PASSWORD: 'smtp-password' NEXT_PRIVATE_SIGNING_PASSPHRASE: 'cert-passphrase' --- apiVersion: apps/v1 kind: Deployment metadata: name: documenso namespace: documenso spec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: documenso template: metadata: labels: app.kubernetes.io/name: documenso spec: securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 containers: - name: documenso image: documenso/documenso:latest ports: - name: http containerPort: 3000 envFrom: - configMapRef: name: documenso-config - secretRef: name: documenso-secrets resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1000m memory: 1Gi livenessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 startupProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 volumeMounts: - name: signing-cert mountPath: /opt/documenso/cert.p12 subPath: cert.p12 readOnly: true volumes: - name: signing-cert secret: secretName: documenso-signing-cert --- apiVersion: v1 kind: Service metadata: name: documenso namespace: documenso spec: type: ClusterIP ports: - name: http port: 80 targetPort: http selector: app.kubernetes.io/name: documenso --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: documenso namespace: documenso annotations: nginx.ingress.kubernetes.io/proxy-body-size: '50m' cert-manager.io/cluster-issuer: 'letsencrypt-prod' spec: ingressClassName: nginx tls: - hosts: - sign.example.com secretName: documenso-tls rules: - host: sign.example.com http: paths: - path: / pathType: Prefix backend: service: name: documenso port: name: http --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: documenso namespace: documenso spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: documenso minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: documenso namespace: documenso spec: minAvailable: 1 selector: matchLabels: app.kubernetes.io/name: documenso ``` --- ## See Also - [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference - [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing - [Storage Configuration](/docs/self-hosting/configuration/storage) - Configure S3-compatible storage - [Email Configuration](/docs/self-hosting/configuration/email) - Configure SMTP providers - [Backups](/docs/self-hosting/maintenance/backups) - Database backup strategies - [Upgrades](/docs/self-hosting/maintenance/upgrades) - Upgrade procedures