Skip to main content
This guide provides instructions for deploying Draftable API Self-Hosted v3 on a generic Kubernetes cluster using local file storage. It is platform-agnostic and applies to any Kubernetes distribution (e.g. EKS, GKE, AKS, on-premises, Rancher, OpenShift).
Generic guidance: Due to the broad nature and diversity of Kubernetes distributions, cluster configurations, and infrastructure providers, this guide is generic in scope. The manifests and instructions provided are intended as a reference starting point and may require adaptation for your specific environment — including StorageClass names, Ingress controller annotations, resource limits, security policies, and networking configuration. Your infrastructure team should review and adjust the manifests to align with your organisation’s Kubernetes standards and operational requirements.
AWS EKS with S3 storage: If you are deploying on AWS EKS and wish to use S3 for file storage, refer to the dedicated AWS EKS Deployment Guide instead, which covers IRSA, S3 configuration, and ALB Ingress.

Prerequisites

Architecture

Configuration

Deploy

Verify

Troubleshooting


Prerequisites

Required tools

Required information

  • A running Kubernetes cluster with kubectl configured
  • Draftable container images available (either pulled from the Draftable registry or pre-loaded into your cluster)
  • Draftable product license key
  • Domain name for the application (if exposing externally)

Hardware requirements

Your cluster nodes should meet these minimum requirements:
  • CPU: 4 vCPU total available for Draftable workloads
  • Memory: 8 GB RAM total available
  • Storage: PersistentVolume provisioner available in your cluster

Architecture overview

Component architecture

Draftable API Self-Hosted consists of several microservices:

Stateful vs stateless components

Production deployments: For production, you should use managed database and caching services provided by your cloud platform (e.g. Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL) rather than running stateful workloads inside Kubernetes. Pod restarts, node failures, or cluster updates can result in data loss for in-cluster databases.
The application components (Web, Celery, Compare, Converter) are stateless and well-suited for Kubernetes. The infrastructure components (PostgreSQL, Redis, RabbitMQ) are stateful — the manifests below run them in-cluster for simplicity, but managed services are recommended for production.

Shared storage requirement

When using FILE_STORAGE_TYPE=local, the Web, Celery Worker, Celery Beat, and Compare containers must all have access to the same shared filesystem at /srv/draftable. In Kubernetes, this requires a ReadWriteMany (RWX) PersistentVolume — for example, NFS, CephFS, or a cloud-native file share.
NFS performance: Draftable does not test or optimise against NFS storage. While functional, NFS-based deployments may experience performance issues under comparison workloads. If your platform supports S3-compatible object storage, consider using FILE_STORAGE_TYPE=s3 instead. See File Storage for details.

Configuration

Namespace

Create a dedicated namespace for Draftable resources:

Secrets

Create a Kubernetes Secret containing sensitive configuration. Replace the placeholder values with your own:
Change all default passwords before deploying to production. The django-secret-key should be a random alphanumeric string of at least 64 characters.

ConfigMap

Create a ConfigMap with application configuration. Update the placeholder values for your environment:
For a complete reference of all available environment variables, see the Docker Compose Guide.

Deploy Draftable

Shared storage

Create a PersistentVolumeClaim for the shared application data volume. This must use a ReadWriteMany access mode so it can be mounted by multiple pods simultaneously:
Uncomment and set storageClassName to match your cluster’s RWX-capable StorageClass (e.g. nfs, efs-sc, cephfs). If your cluster has a default RWX StorageClass, you can omit this field.

Infrastructure services

The Redis service is intentionally named redis-svc rather than redis. Kubernetes automatically creates an environment variable REDIS_PORT=tcp://ip:port for any service named redis, which conflicts with the integer port value expected by the application.
Redis persistence is critical. Redis stores the activated license data and any custom fonts uploaded to the application. If Redis data is lost (e.g. pod restart without persistent storage), all users will be locked out until the license is reactivated. Always use a PersistentVolumeClaim for Redis data, and for production, use a managed Redis service with persistence enabled.If Redis data is lost, re-run the web-init job to reactivate the license from the DRAFTABLE_PRODUCT_KEY environment variable:
You do not need to restart the entire deployment — only the web-init job needs to run.

Database migrations

Run the database migration job before deploying application services:

Application services

Only one replica: Celery Beat must run as a single replica. Running multiple instances will cause duplicate task scheduling.
The Compare service is a .NET application with different environment variable requirements than the Python-based web containers. It connects directly to RabbitMQ and Redis but does not connect to PostgreSQL.
TCP probes only: The converter must use TCP socket probes, not HTTP probes. The JODConverter REST API returns 404 on all health endpoints (/, /health, /actuator/health), causing HTTP probes to fail and triggering endless restart loops. TCP probes verify port 8080 is listening, which succeeds once Tomcat starts — even while LibreOffice is still initialising.
During normal startup, you may see “Office process died with exit code 81” messages in the converter logs. This is expected behaviour while LibreOffice initialises and typically resolves within 90–120 seconds.

Ingress

Configure an Ingress to expose the web service externally. The example below uses a generic Ingress resource — adapt the annotations and TLS configuration for your Ingress controller (e.g. nginx-ingress, Traefik, HAProxy):
To use TLS, create a Kubernetes Secret with your certificate:

Deployment order

Apply the manifests in the following order:
1

Create namespace and configuration

2

Deploy infrastructure services

Wait for all infrastructure pods to become ready:
3

Run database migrations

4

Deploy application services

5

Deploy Ingress


Verification

Check pod status

All pods should show Running status with ready containers:

Check services

Access the application

Once the Ingress is configured and DNS is pointing to your cluster, open https://draftable.yourcompany.com in your browser. For testing without Ingress, use port-forwarding:
Then visit http://localhost:8000.

Troubleshooting

Pods stuck in Pending

Cause: PersistentVolumeClaims not binding. Solution: Verify your cluster has a StorageClass that supports the required access modes:
For the shared draftable-data PVC, ensure your StorageClass supports ReadWriteMany.

CreateContainerConfigError

Cause: ConfigMap or Secret key not found. Solution: Verify all ConfigMap and Secret keys exist:

Users locked out after Redis restart

Cause: Redis stores the activated license. If Redis data is lost (pod restart without persistent storage, PVC deletion, or cache flush), the license is no longer available and all users are locked out. Solution: Re-run the web-init job to reactivate the license from the DRAFTABLE_PRODUCT_KEY environment variable:
To prevent this in future, ensure Redis is using persistent storage (PVC or managed service) with persistence enabled (AOF or RDB snapshots).

REDIS_PORT parse error (invalid literal for int())

Cause: Kubernetes auto-creates REDIS_PORT=tcp://ip:port when a service is named redis. Solution: The Redis service in these manifests is named redis-svc to avoid this conflict. Ensure you are using redis-svc as the service name.

Converter in CrashLoopBackOff

Cause: HTTP health probes fail because JODConverter returns 404 on all health endpoints, and LibreOffice takes 60–90 seconds to initialise. Solution: Ensure the converter uses TCP socket probes, not HTTP probes. See the Converter manifest above.

DisallowedHost error in web logs

Cause: Requests arriving with a hostname not in ALLOWED_HOSTS. Solution: Set ALLOWED_HOSTS: "*" in the ConfigMap, or add all hostnames that will be used to access the application (including any health check hostnames used by your Ingress controller or load balancer).

Useful debugging commands


Using managed services

For production deployments, replace in-cluster stateful services with managed services provided by your platform: When using managed services:
  1. Skip applying the corresponding in-cluster manifest (e.g. don’t apply 10-postgresql.yaml)
  2. Update the ConfigMap with the managed service connection details:
  3. If TLS is required, set the corresponding TLS variable (e.g. AMQP_TLS: "true", REDIS_TLS: "true")

Configuration reference

For a complete list of all environment variables including security, CORS, HSTS, logging, and S3 storage options, see the Docker Compose Guide – Environment Variables.

Support

If you encounter issues during deployment, please contact us at support@draftable.com with:
  • Pod logs (kubectl logs -n draftable deployment/<pod-name>)
  • Pod events (kubectl describe pod -n draftable -l app=<app-name>)
  • ConfigMap values (sanitised)
  • Error messages from the browser console