---
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'
```

<Callout type="warn">
  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.
</Callout>

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
```

<Callout type="info">
  Pin to a specific image tag (e.g., `documenso/documenso:<version>`) in production instead of `latest`
  to ensure predictable deployments.
</Callout>

### 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
```

<Callout type="warn">
  In-cluster PostgreSQL is not recommended for production. It lacks high availability, automated
  backups, and point-in-time recovery that managed services provide.
</Callout>

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

<Accordions type="multiple">
  <Accordion title="Pods not starting">
    Check pod status: 
    
    ```bash
    # Get pod status
    kubectl get pods -n documenso
    
    # Describe pod
    kubectl describe pod <pod-name> -n documenso
    ```
    
    Common causes: 
    - ImagePullBackOff (check image name and registry access)
    - CrashLoopBackOff (check logs for application errors)
    - Pending (check resource requests and node capacity)
  </Accordion>
  <Accordion title="Database connection errors">
    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"
    ```
  </Accordion>
  <Accordion title="Ingress not working">
    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
    ```
  </Accordion>
  <Accordion title="Certificate errors">
    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 <pod-name> -n documenso -- ls -la /opt/documenso/
    ```
  </Accordion>
  <Accordion title="Health check failures">
    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
    ```
  </Accordion>
  <Accordion title="Rolling back a deployment">
    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
    ```

  </Accordion>
</Accordions>

## 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
