Documentation Index Fetch the complete documentation index at: https://help.draftable.com/llms.txt
Use this file to discover all available pages before exploring further.
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
Tool Purpose Installation kubectl Kubernetes cluster management Download
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:
Component Image Purpose Ports Web draftable/apish-webDjango web application, API endpoints 8000 Celery Worker draftable/apish-webBackground task processing — Celery Beat draftable/apish-webTask scheduler — Compare draftable/apish-compareDocument comparison engine (.NET) — (communicates via AMQP) Converter draftable/apish-converterOffice-to-PDF conversion (LibreOffice) 8080 PostgreSQL postgres:16-alpineDatabase 5432 Redis redis:8-alpineCaching, sessions, license storage 6379 RabbitMQ rabbitmq:4-management-alpineMessage broker 5672
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:
# 00-namespace.yaml
apiVersion : v1
kind : Namespace
metadata :
name : draftable
Secrets
Create a Kubernetes Secret containing sensitive configuration. Replace the placeholder values with your own:
# 01-secrets.yaml
apiVersion : v1
kind : Secret
metadata :
name : draftable-secrets
namespace : draftable
type : Opaque
stringData :
db-password : "YOUR_SECURE_DB_PASSWORD"
amqp-password : "YOUR_SECURE_AMQP_PASSWORD"
redis-password : "YOUR_SECURE_REDIS_PASSWORD"
django-secret-key : "YOUR_64_CHARACTER_RANDOM_STRING"
draftable-product-key : "YOUR-DRAFTABLE-LICENSE-KEY"
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:
# 02-configmap.yaml
apiVersion : v1
kind : ConfigMap
metadata :
name : draftable-config
namespace : draftable
data :
# Application
APP_BASE_URL : "https://draftable.yourcompany.com"
ENV : "prod"
ALLOWED_HOSTS : "*"
CSRF_TRUSTED_ORIGINS : "https://draftable.yourcompany.com"
REQUIRE_HTTPS : "true"
# Database
DB_HOST : "postgresql-svc"
DB_PORT : "5432"
DB_NAME : "draftable"
DB_USER : "postgres"
# Redis
REDIS_HOST : "redis-svc"
REDIS_PORT : "6379"
# RabbitMQ
AMQP_HOST : "rabbitmq-svc"
AMQP_PORT : "5672"
AMQP_USER : "draftable"
# File storage
FILE_STORAGE_TYPE : "local"
# Compare
COMPARE_WORKERS_COUNT : "1"
JODCONVERTER_URL : "http://converter-svc:8080/lool/convert-to/pdf"
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:
# 05-shared-storage.yaml
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
name : draftable-data
namespace : draftable
spec :
accessModes :
- ReadWriteMany
resources :
requests :
storage : 20Gi
# storageClassName: your-rwx-storage-class
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
# 10-postgresql.yaml
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
name : postgresql-data
namespace : draftable
spec :
accessModes :
- ReadWriteOnce
resources :
requests :
storage : 10Gi
---
apiVersion : apps/v1
kind : Deployment
metadata :
name : postgresql
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : postgresql
template :
metadata :
labels :
app : postgresql
spec :
containers :
- name : postgresql
image : postgres:16-alpine
ports :
- containerPort : 5432
env :
- name : POSTGRES_DB
value : "draftable"
- name : POSTGRES_USER
value : "postgres"
- name : POSTGRES_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : db-password
volumeMounts :
- name : data
mountPath : /var/lib/postgresql/data
subPath : pgdata
readinessProbe :
exec :
command : [ "pg_isready" , "-U" , "postgres" , "-d" , "draftable" ]
initialDelaySeconds : 10
periodSeconds : 5
resources :
requests :
cpu : 250m
memory : 256Mi
limits :
cpu : "1"
memory : 1Gi
volumes :
- name : data
persistentVolumeClaim :
claimName : postgresql-data
---
apiVersion : v1
kind : Service
metadata :
name : postgresql-svc
namespace : draftable
spec :
selector :
app : postgresql
ports :
- port : 5432
targetPort : 5432
# 11-rabbitmq.yaml
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
name : rabbitmq-data
namespace : draftable
spec :
accessModes :
- ReadWriteOnce
resources :
requests :
storage : 5Gi
---
apiVersion : apps/v1
kind : Deployment
metadata :
name : rabbitmq
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : rabbitmq
template :
metadata :
labels :
app : rabbitmq
spec :
containers :
- name : rabbitmq
image : rabbitmq:4-management-alpine
ports :
- containerPort : 5672
- containerPort : 15672
env :
- name : RABBITMQ_DEFAULT_USER
value : "draftable"
- name : RABBITMQ_DEFAULT_PASS
valueFrom :
secretKeyRef :
name : draftable-secrets
key : amqp-password
volumeMounts :
- name : data
mountPath : /var/lib/rabbitmq
readinessProbe :
exec :
command : [ "rabbitmq-diagnostics" , "-q" , "ping" ]
initialDelaySeconds : 30
periodSeconds : 10
resources :
requests :
cpu : 250m
memory : 256Mi
limits :
cpu : "1"
memory : 1Gi
volumes :
- name : data
persistentVolumeClaim :
claimName : rabbitmq-data
---
apiVersion : v1
kind : Service
metadata :
name : rabbitmq-svc
namespace : draftable
spec :
selector :
app : rabbitmq
ports :
- name : amqp
port : 5672
targetPort : 5672
- name : management
port : 15672
targetPort : 15672
# 12-redis.yaml
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
name : redis-data
namespace : draftable
spec :
accessModes :
- ReadWriteOnce
resources :
requests :
storage : 2Gi
---
apiVersion : apps/v1
kind : Deployment
metadata :
name : redis
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : redis
template :
metadata :
labels :
app : redis
spec :
containers :
- name : redis
image : redis:8-alpine
ports :
- containerPort : 6379
command :
- "redis-server"
- "--requirepass"
- "$(REDIS_PASSWORD)"
- "--appendonly"
- "yes"
env :
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
volumeMounts :
- name : data
mountPath : /data
readinessProbe :
exec :
command : [ "sh" , "-c" , "redis-cli -a $REDIS_PASSWORD ping" ]
initialDelaySeconds : 10
periodSeconds : 5
resources :
requests :
cpu : 100m
memory : 128Mi
limits :
cpu : 500m
memory : 512Mi
volumes :
- name : data
persistentVolumeClaim :
claimName : redis-data
---
apiVersion : v1
kind : Service
metadata :
name : redis-svc
namespace : draftable
spec :
selector :
app : redis
ports :
- port : 6379
targetPort : 6379
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: kubectl delete job web-init -n draftable --ignore-not-found
kubectl apply -f 20-web-init-job.yaml
kubectl wait --for=condition=complete job/web-init -n draftable --timeout=300s
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:
# 20-web-init-job.yaml
apiVersion : batch/v1
kind : Job
metadata :
name : web-init
namespace : draftable
spec :
template :
spec :
restartPolicy : OnFailure
containers :
- name : web-init
image : draftable/apish-web:latest
env :
- name : APP_MODE
value : "web_init"
- name : DB_PASS
valueFrom :
secretKeyRef :
name : draftable-secrets
key : db-password
- name : AMQP_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : amqp-password
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
- name : DJANGO_SECRET_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : django-secret-key
- name : DRAFTABLE_PRODUCT_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : draftable-product-key
envFrom :
- configMapRef :
name : draftable-config
volumeMounts :
- name : app-data
mountPath : /srv/draftable
volumes :
- name : app-data
persistentVolumeClaim :
claimName : draftable-data
Application services
# 21-web.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : web
namespace : draftable
spec :
replicas : 2
selector :
matchLabels :
app : web
template :
metadata :
labels :
app : web
spec :
containers :
- name : web
image : draftable/apish-web:latest
ports :
- containerPort : 8000
env :
- name : APP_MODE
value : "web"
- name : DB_PASS
valueFrom :
secretKeyRef :
name : draftable-secrets
key : db-password
- name : AMQP_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : amqp-password
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
- name : DJANGO_SECRET_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : django-secret-key
- name : DRAFTABLE_PRODUCT_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : draftable-product-key
envFrom :
- configMapRef :
name : draftable-config
volumeMounts :
- name : app-data
mountPath : /srv/draftable
readinessProbe :
httpGet :
path : /api/?healthcheck
port : 8000
initialDelaySeconds : 30
periodSeconds : 10
livenessProbe :
httpGet :
path : /api/?healthcheck
port : 8000
initialDelaySeconds : 60
periodSeconds : 30
resources :
requests :
cpu : 250m
memory : 512Mi
limits :
cpu : "1"
memory : 2Gi
volumes :
- name : app-data
persistentVolumeClaim :
claimName : draftable-data
---
apiVersion : v1
kind : Service
metadata :
name : web-svc
namespace : draftable
spec :
selector :
app : web
ports :
- port : 8000
targetPort : 8000
# 22-celery-worker.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : celery-worker
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : celery-worker
template :
metadata :
labels :
app : celery-worker
spec :
containers :
- name : celery-worker
image : draftable/apish-web:latest
env :
- name : APP_MODE
value : "celery_worker"
- name : DB_PASS
valueFrom :
secretKeyRef :
name : draftable-secrets
key : db-password
- name : AMQP_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : amqp-password
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
- name : DJANGO_SECRET_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : django-secret-key
- name : DRAFTABLE_PRODUCT_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : draftable-product-key
envFrom :
- configMapRef :
name : draftable-config
volumeMounts :
- name : app-data
mountPath : /srv/draftable
resources :
requests :
cpu : 250m
memory : 512Mi
limits :
cpu : "1"
memory : 2Gi
volumes :
- name : app-data
persistentVolumeClaim :
claimName : draftable-data
# 23-celery-beat.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : celery-beat
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : celery-beat
template :
metadata :
labels :
app : celery-beat
spec :
containers :
- name : celery-beat
image : draftable/apish-web:latest
env :
- name : APP_MODE
value : "celery_beat"
- name : DB_PASS
valueFrom :
secretKeyRef :
name : draftable-secrets
key : db-password
- name : AMQP_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : amqp-password
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
- name : DJANGO_SECRET_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : django-secret-key
- name : DRAFTABLE_PRODUCT_KEY
valueFrom :
secretKeyRef :
name : draftable-secrets
key : draftable-product-key
envFrom :
- configMapRef :
name : draftable-config
volumeMounts :
- name : app-data
mountPath : /srv/draftable
resources :
requests :
cpu : 100m
memory : 256Mi
limits :
cpu : 500m
memory : 512Mi
volumes :
- name : app-data
persistentVolumeClaim :
claimName : draftable-data
Only one replica : Celery Beat must run as a single replica. Running multiple instances will cause duplicate task scheduling.
# 24-compare.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : compare
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : compare
template :
metadata :
labels :
app : compare
spec :
containers :
- name : compare
image : draftable/apish-compare:latest
env :
- name : COMPARE_WORKERS_COUNT
valueFrom :
configMapKeyRef :
name : draftable-config
key : COMPARE_WORKERS_COUNT
- name : APP_BASE_URL
valueFrom :
configMapKeyRef :
name : draftable-config
key : APP_BASE_URL
- name : AMQP_HOST
valueFrom :
configMapKeyRef :
name : draftable-config
key : AMQP_HOST
- name : AMQP_PORT
valueFrom :
configMapKeyRef :
name : draftable-config
key : AMQP_PORT
- name : AMQP_USER
valueFrom :
configMapKeyRef :
name : draftable-config
key : AMQP_USER
- name : AMQP_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : amqp-password
- name : REDIS_HOST
valueFrom :
configMapKeyRef :
name : draftable-config
key : REDIS_HOST
- name : REDIS_PORT
valueFrom :
configMapKeyRef :
name : draftable-config
key : REDIS_PORT
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
- name : JODCONVERTER_URL
valueFrom :
configMapKeyRef :
name : draftable-config
key : JODCONVERTER_URL
volumeMounts :
- name : app-data
mountPath : /srv/draftable
- name : tmp
mountPath : /tmp/Draftable
livenessProbe :
exec :
command : [ "sh" , "-c" , "ps aux | grep '[C]ompareService' || exit 1" ]
initialDelaySeconds : 30
periodSeconds : 15
resources :
requests :
cpu : "1"
memory : 2Gi
limits :
cpu : "2"
memory : 4Gi
volumes :
- name : app-data
persistentVolumeClaim :
claimName : draftable-data
- name : tmp
emptyDir : {}
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.
# 25-converter.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : converter
namespace : draftable
spec :
replicas : 1
selector :
matchLabels :
app : converter
template :
metadata :
labels :
app : converter
spec :
containers :
- name : converter
image : draftable/apish-converter:latest
ports :
- containerPort : 8080
env :
- name : REDIS_HOST
valueFrom :
configMapKeyRef :
name : draftable-config
key : REDIS_HOST
- name : REDIS_PORT
valueFrom :
configMapKeyRef :
name : draftable-config
key : REDIS_PORT
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : draftable-secrets
key : redis-password
livenessProbe :
tcpSocket :
port : 8080
initialDelaySeconds : 120
periodSeconds : 30
timeoutSeconds : 10
failureThreshold : 3
readinessProbe :
tcpSocket :
port : 8080
initialDelaySeconds : 90
periodSeconds : 10
timeoutSeconds : 5
failureThreshold : 6
resources :
requests :
cpu : 500m
memory : 1Gi
limits :
cpu : "2"
memory : 4Gi
---
apiVersion : v1
kind : Service
metadata :
name : converter-svc
namespace : draftable
spec :
selector :
app : converter
ports :
- port : 8080
targetPort : 8080
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):
# 30-ingress.yaml
apiVersion : networking.k8s.io/v1
kind : Ingress
metadata :
name : draftable-ingress
namespace : draftable
annotations :
# Adapt these annotations for your Ingress controller
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# nginx.ingress.kubernetes.io/proxy-body-size: "100m"
spec :
# ingressClassName: nginx
tls :
- hosts :
- draftable.yourcompany.com
secretName : draftable-tls
rules :
- host : draftable.yourcompany.com
http :
paths :
- path : /
pathType : Prefix
backend :
service :
name : web-svc
port :
number : 8000
To use TLS, create a Kubernetes Secret with your certificate: kubectl create secret tls draftable-tls -n draftable \
--cert=/path/to/tls.crt \
--key=/path/to/tls.key
Deployment order
Apply the manifests in the following order:
Create namespace and configuration
kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-secrets.yaml
kubectl apply -f 02-configmap.yaml
kubectl apply -f 05-shared-storage.yaml
Deploy infrastructure services
kubectl apply -f 10-postgresql.yaml
kubectl apply -f 11-rabbitmq.yaml
kubectl apply -f 12-redis.yaml
Wait for all infrastructure pods to become ready: kubectl wait --for=condition=ready pod -l app=postgresql -n draftable --timeout=300s
kubectl wait --for=condition=ready pod -l app=rabbitmq -n draftable --timeout=300s
kubectl wait --for=condition=ready pod -l app=redis -n draftable --timeout=300s
Run database migrations
kubectl apply -f 20-web-init-job.yaml
kubectl wait --for=condition=complete job/web-init -n draftable --timeout=300s
Deploy application services
kubectl apply -f 21-web.yaml
kubectl apply -f 22-celery-worker.yaml
kubectl apply -f 23-celery-beat.yaml
kubectl apply -f 24-compare.yaml
kubectl apply -f 25-converter.yaml
Deploy Ingress
kubectl apply -f 30-ingress.yaml
Verification
Check pod status
kubectl get pods -n draftable
All pods should show Running status with ready containers:
NAME READY STATUS RESTARTS AGE
celery-beat-xxx 1/1 Running 0 5m
celery-worker-xxx 1/1 Running 0 5m
compare-xxx 1/1 Running 0 5m
converter-xxx 1/1 Running 0 5m
postgresql-xxx 1/1 Running 0 10m
rabbitmq-xxx 1/1 Running 0 10m
redis-xxx 1/1 Running 0 10m
web-xxx 1/1 Running 0 5m
web-xxx 1/1 Running 0 5m
Check services
kubectl get svc -n draftable
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:
kubectl port-forward svc/web-svc 8000:8000 -n draftable
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:
kubectl get storageclass
kubectl get pvc -n draftable
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:
kubectl get configmap draftable-config -n draftable -o yaml
kubectl get secret draftable-secrets -n draftable -o yaml
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:
kubectl delete job web-init -n draftable --ignore-not-found
kubectl apply -f 20-web-init-job.yaml
kubectl wait --for=condition=complete job/web-init -n draftable --timeout=300s
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
# View all pods
kubectl get pods -n draftable
# View pod logs
kubectl logs -n draftable deployment/web --tail=100
# Describe pod for events
kubectl describe pod -n draftable -l app=web
# Check environment variables in a pod
kubectl exec -n draftable deployment/web -- env | sort
# Restart a deployment
kubectl rollout restart deployment/web -n draftable
Using managed services
For production deployments, replace in-cluster stateful services with managed services provided by your platform:
Component In-Cluster (Dev/Test) Production Alternative PostgreSQL 10-postgresql.yamlManaged PostgreSQL (e.g. Amazon RDS, Google Cloud SQL, Azure Database) Redis 12-redis.yamlManaged Redis (e.g. Amazon ElastiCache, Google Memorystore, Azure Cache) RabbitMQ 11-rabbitmq.yamlManaged RabbitMQ (e.g. Amazon MQ, CloudAMQP)
When using managed services:
Skip applying the corresponding in-cluster manifest (e.g. don’t apply 10-postgresql.yaml)
Update the ConfigMap with the managed service connection details:
DB_HOST : "your-managed-postgres-hostname"
DB_PORT : "5432"
DB_TLS_MODE : "require"
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