Skip to main content
This guide provides step-by-step instructions for deploying Draftable API Self-Hosted v3 on AWS EKS with S3 storage.
AWS Only: Kubernetes deployment of Draftable API Self-Hosted v3 is currently supported on AWS EKS only. Azure AKS is not currently documented or supported due to the lack of native Azure Blob Storage support in the application — see FILE_STORAGE_TYPE for details.
S3 Storage Only: This guide covers S3-based deployments. EFS storage configurations are not covered in this documentation and are not recommended for Draftable deployments.

Architecture Overview

Component Architecture

Draftable API Self-Hosted consists of several microservices that work together:
ComponentPurposeKubernetes Suitability
WebDjango web application, API endpoints✅ Excellent (stateless)
Celery WorkerBackground task processing✅ Excellent (stateless)
Celery BeatTask scheduler✅ Excellent (stateless)
CompareDocument comparison engine (.NET)✅ Excellent (stateless)
ConverterOffice-to-PDF conversion (LibreOffice)✅ Excellent (stateless)
PostgreSQLDatabase⚠️ Consider managed service
RedisCaching and sessions⚠️ Consider managed service
RabbitMQMessage broker⚠️ Consider managed service

Stateful vs Stateless Recommendation

Critical Data Loss Risk: The stateful components (PostgreSQL, Redis, RabbitMQ) included in this guide use Kubernetes PersistentVolumeClaims for storage. However, if pods are restarted, rescheduled, or the cluster is rebuilt, you may lose all your data including the database, cached sessions, and queued messages.For production deployments, you must use managed AWS services for stateful components.
ComponentKubernetes (Dev/Test Only)Production Requirement
PostgreSQL10-postgresql.yamlAmazon RDS for PostgreSQL
Redis12-redis.yamlAmazon ElastiCache for Redis
RabbitMQ11-rabbitmq.yamlAmazon MQ for RabbitMQ
Why managed services?
  • Kubernetes frequently tears down and recreates containers—excellent for stateless services but dangerous for stateful workloads
  • Pod restarts, node failures, or cluster updates can result in permanent data loss
  • Managed services provide automated backups, high availability, and data persistence guarantees

S3 Authentication Architecture

Critical: The Compare service does NOT support IRSA (IAM Roles for Service Accounts). It requires explicit AWS credentials.
ComponentService AccountS3 Authentication Method
Webdraftable-s3-saIRSA (IAM Roles for Service Accounts)
Celery Workerdraftable-s3-saIRSA
Celery Beatdraftable-s3-saIRSA
ComparedefaultExplicit AWS credentials (required!)
The Compare service (.NET application) is missing the AWSSDK.SecurityToken assembly required for AssumeRoleWithWebIdentity. Attempting to use IRSA will result in:
Assembly AWSSDK.SecurityToken could not be found or loaded.
This assembly must be available at runtime to use Amazon.Runtime.AssumeRoleAWSCredentials.

Prerequisites

Required Information

Before starting, gather the following:
  • AWS Account ID
  • Desired AWS Region (e.g., ap-southeast-2)
  • Domain name for the application
  • Draftable product license key

Required Tools

Install and configure the following tools on your workstation:
ToolPurposeInstallation
AWS CLIAWS resource managementDownload
kubectlKubernetes cluster managementDownload
eksctlEKS cluster creationDownload
HelmKubernetes package managerDownload
Verify installations:
aws --version
kubectl version --client
eksctl version
helm version

Hardware Requirements

Your EKS cluster nodes should meet these minimum requirements:
  • Instance Type: t3.large or larger (2 vCPU, 8 GB RAM per node)
  • Node Count: Minimum 3 nodes for production
  • Storage: 50 GB per node

Estimated Deployment Time

PhaseDuration
EKS cluster creation15–25 minutes
S3 and IAM setup10–15 minutes
Application deployment10–15 minutes
Total45–60 minutes

AWS Account Setup

IAM Access Configuration

Recommendation: Instead of creating a dedicated IAM user for EKS access, grant access to your existing administrator role (e.g., SSO Administrator). This allows you to use your regular admin credentials with kubectl and view resources in the AWS Console.
After cluster creation, add your administrator role via EKS Access Entries:
  1. Go to AWS ConsoleEKS → Your Cluster → Access tab
  2. Click Create access entry
  3. Select your SSO Administrator role (or equivalent)
  4. Click Next
  5. Add access policy: AmazonEKSClusterAdminPolicy
  6. Click Create

Option B: Create Dedicated IAM User

If you prefer a dedicated user, it needs these permissions:
  • AdministratorAccess (recommended for initial setup)
Or these specific policies:
  • AmazonEKSClusterPolicy
  • AmazonEKSServicePolicy
  • AmazonEC2FullAccess
  • AmazonVPCFullAccess
  • AWSCloudFormationFullAccess
  • IAMFullAccess

Configure AWS CLI

aws configure
# Enter: Access Key ID, Secret Access Key, Region, Output format (json)

Create EKS Cluster

Cluster Configuration

Download or create the cluster configuration file:

eksctl-cluster-config.yaml

EKS cluster configuration for Draftable
Key configuration points:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: draftable-eks
  region: ap-southeast-2  # UPDATE: Your region
  version: "1.31"  # UPDATE: Use latest stable version

vpc:
  nat:
    gateway: Single  # Avoids Elastic IP limits

iam:
  withOIDC: true  # REQUIRED for IRSA (S3 access)

managedNodeGroups:
  - name: draftable-workers
    instanceType: t3.large
    desiredCapacity: 3
    minSize: 2
    maxSize: 5
    volumeSize: 50
    volumeType: gp3
Kubernetes Version: Always use the latest stable version. Check available versions with:
aws eks describe-addon-versions --query 'addons[0].addonVersions[*].compatibilities[*].clusterVersion' --output text | sort -u

Create the Cluster

eksctl create cluster -f eksctl-cluster-config.yaml
This command takes 15–25 minutes to complete. Do not interrupt the process.

Configure kubectl

After cluster creation, configure kubectl to connect:
aws eks update-kubeconfig --region ap-southeast-2 --name draftable-eks
To view cluster resources in the AWS Console:
  1. Go to AWS ConsoleEKSdraftable-eksAccess tab
  2. Click Create access entry
  3. Select your SSO Administrator role
  4. Add policy: AmazonEKSClusterAdminPolicy
  5. Click Create

Verify Cluster

kubectl get nodes
You should see 3 nodes in Ready state.

Create S3 Bucket

Create the Bucket

# Set variables
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
BUCKET_NAME="draftable-storage-$ACCOUNT_ID"
REGION="ap-southeast-2"

# Create bucket
aws s3 mb s3://$BUCKET_NAME --region $REGION

Configure CORS

CORS is required for the document viewer to load files from S3. Without CORS, comparisons will fail to render in the browser.
Download or create the CORS configuration:

s3-bucket-cors.json

S3 CORS configuration
Update the AllowedOrigins to match your domain:
[
    {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["GET", "HEAD"],
        "AllowedOrigins": ["https://your-domain.example.com"],
        "ExposeHeaders": ["ETag", "Content-Length", "Content-Type"]
    }
]
Apply the CORS configuration:
aws s3api put-bucket-cors --bucket $BUCKET_NAME --cors-configuration file://s3-bucket-cors.json

Configure IAM for S3 Access

Draftable requires two types of AWS credentials:
  1. IRSA (IAM Role for Service Accounts) – Used by Web, Celery Worker, and Celery Beat
  2. IAM User with Access Keys – Required by the Compare service (does not support IRSA)

Get OIDC Provider ID

OIDC_ID=$(aws eks describe-cluster --name draftable-eks --region ap-southeast-2 \
  --query "cluster.identity.oidc.issuer" --output text | rev | cut -d'/' -f1 | rev)
echo $OIDC_ID

Create S3 Access Policy

Download or create the policy:

s3-access-policy.json

IAM policy for S3 access
Update the bucket name in the policy, then create it:
aws iam create-policy \
  --policy-name DraftableS3AccessPolicy \
  --policy-document file://s3-access-policy.json

Create IAM Role for IRSA (Web/Celery)

Download or create the trust policy:

irsa-trust-policy.json

IRSA trust policy template
Update the following placeholders:
  • YOUR_ACCOUNT_ID – Your AWS account ID
  • YOUR_REGION – Your AWS region
  • YOUR_OIDC_ID – The OIDC ID from the previous step
Create the role:
aws iam create-role \
  --role-name DraftableS3AccessRole \
  --assume-role-policy-document file://irsa-trust-policy.json

aws iam attach-role-policy \
  --role-name DraftableS3AccessRole \
  --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/DraftableS3AccessPolicy

Create IAM User for Compare Service

Required: The Compare service cannot use IRSA and must have explicit AWS credentials.
# Create user
aws iam create-user --user-name draftable-s3-user

# Attach policy
aws iam put-user-policy \
  --user-name draftable-s3-user \
  --policy-name S3Access \
  --policy-document file://s3-access-policy.json

# Create access key
aws iam create-access-key --user-name draftable-s3-user
Save the AccessKeyId and SecretAccessKey from the output – you will need these when creating Kubernetes secrets.

Install AWS Load Balancer Controller

The AWS Load Balancer Controller is required to create Application Load Balancers (ALB) for the Ingress.

Create IAM Policy

curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json

aws iam create-policy \
  --policy-name AWSLoadBalancerControllerIAMPolicy \
  --policy-document file://iam_policy.json

Create Service Account

eksctl create iamserviceaccount \
  --cluster=draftable-eks \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
  --approve \
  --region=ap-southeast-2

Install Controller via Helm

helm repo add eks https://aws.github.io/eks-charts
helm repo update

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=draftable-eks \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller
Verify the controller is running:
kubectl get deployment -n kube-system aws-load-balancer-controller

Deploy Draftable

Kubernetes Manifests

All Kubernetes manifests are available in our GitHub repository:

Kubernetes Manifests for EKS + S3

Complete set of deployment manifests
Templated Files: All YAML manifests are deliberately templated and incomplete. You must replace placeholder values (marked with YOUR_* or REPLACE_*) with your own configuration before applying them to your cluster.
FileDescription
00-namespace.yamlCreates the draftable namespace
01-secrets.yamlApplication secrets (template)
02-configmap.yamlApplication configuration
03-service-account.yamlIRSA service account for S3
04-aws-credentials-secret.yamlAWS credentials for Compare service
10-postgresql.yamlPostgreSQL database
11-rabbitmq.yamlRabbitMQ message broker
12-redis.yamlRedis cache
20-web-init-job.yamlDatabase migration job
21-web.yamlWeb application
22-celery-worker.yamlCelery background worker
23-celery-beat.yamlCelery scheduler
24-compare.yamlCompare service
25-converter.yamlDocument converter (must use TCP probes)
30-ingress.yamlALB Ingress

Update Configuration Files

Before deploying, update these files with your values:

02-configmap.yaml

data:
  APP_BASE_URL: "https://your-domain.example.com"
  SERVER_DNS: "your-domain.example.com"
  CSRF_TRUSTED_ORIGINS: "https://your-domain.example.com"
  S3_STORAGE_BUCKET: "your-bucket-name"
  AWS_S3_REGION_NAME: "ap-southeast-2"
Critical S3 Configuration: The FILE_STORAGE_TYPE must be set to s3. Without this, the application uses local filesystem storage regardless of other S3 settings.

03-service-account.yaml

annotations:
  eks.amazonaws.com/role-arn: arn:aws:iam::YOUR_ACCOUNT_ID:role/DraftableS3AccessRole

30-ingress.yaml

annotations:
  alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-southeast-2:YOUR_ACCOUNT_ID:certificate/YOUR_CERT_ID
spec:
  rules:
    - host: your-domain.example.com

Create Secrets

1

Create namespace

kubectl apply -f 00-namespace.yaml
2

Create application secrets

kubectl create secret generic draftable-secrets -n draftable \
  --from-literal=db-password='YourDbPassword123!' \
  --from-literal=amqp-password='YourAmqpPassword123!' \
  --from-literal=redis-password='YourRedisPassword123!' \
  --from-literal=django-secret-key='your-64-character-random-string-here-make-it-long-and-random!' \
  --from-literal=draftable-product-key='YOUR-DRAFTABLE-LICENSE-KEY'
3

Create AWS credentials for Compare service

kubectl create secret generic aws-s3-credentials -n draftable \
  --from-literal=AWS_ACCESS_KEY_ID='YOUR_ACCESS_KEY' \
  --from-literal=AWS_SECRET_ACCESS_KEY='YOUR_SECRET_KEY'

Deploy Infrastructure

1

Apply configuration

kubectl apply -f 02-configmap.yaml
kubectl apply -f 03-service-account.yaml
2

Deploy infrastructure services

kubectl apply -f 10-postgresql.yaml
kubectl apply -f 11-rabbitmq.yaml
kubectl apply -f 12-redis.yaml
3

Wait for infrastructure pods

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

# Wait for job to complete
kubectl wait --for=condition=complete job/web-init -n draftable --timeout=300s

Deploy Application

Converter Health Probes: The 25-converter.yaml manifest must use TCP socket probes (not HTTP). The JODConverter API returns 404 on all health endpoints, and LibreOffice takes 60-90 seconds to start. Using HTTP probes will cause the pod to enter CrashLoopBackOff with hundreds of restarts. See Troubleshooting for details.
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

# Wait for web pods
kubectl wait --for=condition=ready pod -l app=web -n draftable --timeout=300s
Converter startup time: The converter pod may take 90-120 seconds to become ready. During startup, you may see “Office process died with exit code 81” in the logs—this is normal while LibreOffice initializes.

Deploy Ingress

kubectl apply -f 30-ingress.yaml

Configure DNS and HTTPS

Get ALB DNS Name

kubectl get ingress draftable-ingress -n draftable
The ADDRESS column shows the ALB DNS name.

Create DNS Record

In your DNS provider, create a CNAME record:
FieldValue
NameYour subdomain (e.g., draftable)
TypeCNAME
ValueALB DNS name from above

Request ACM Certificate

If you haven’t already created an ACM certificate:
  1. Go to AWS Certificate Manager in the AWS Console
  2. Click Request a certificate
  3. Select Request a public certificate
  4. Enter your domain name
  5. Choose DNS validation
  6. Complete the DNS validation process
  7. Update 30-ingress.yaml with the certificate ARN

Verification

Check Pod Status

kubectl get pods -n draftable
All pods should show Running status with 1/1 or 2/2 ready. Expected output:
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 S3 Connectivity

After creating a comparison, verify files are stored in S3:
aws s3 ls s3://$BUCKET_NAME/ --recursive

Access the Application

Open https://your-domain.example.com in your browser. You should see the Draftable login page.

Troubleshooting

Pods Stuck in Pending

Cause: PersistentVolumeClaims not binding. Solution: Check StorageClass exists:
kubectl get storageclass
Ensure you’re using gp2 (default on EKS). If using gp3, you may need to create the StorageClass first.

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

Compare Fails with AWSSDK.SecurityToken could not be found

Cause: Compare service is attempting to use IRSA (not supported). Solution:
  1. Ensure Compare uses serviceAccountName: default (not draftable-s3-sa)
  2. Verify aws-s3-credentials secret exists with AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
kubectl get deployment compare -n draftable -o jsonpath='{.spec.template.spec.serviceAccountName}'
# Should output: default

kubectl get secret aws-s3-credentials -n draftable
# Should exist

Viewer Doesn’t Render Documents (CORS Error)

Cause: S3 bucket CORS not configured. Solution: Apply CORS configuration:
aws s3api put-bucket-cors --bucket $BUCKET_NAME --cors-configuration file://s3-bucket-cors.json

DisallowedHost Error in Web Logs

Cause: ALB health checks use the pod’s internal IP address, not the domain name. Solution: Ensure ALLOWED_HOSTS: "*" is set in the ConfigMap (already configured in the provided manifests).

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 is named redis-svc in our manifests to avoid this conflict. Ensure you’re using the provided manifests.

Converter in CrashLoopBackOff (400+ Restarts)

Cause: HTTP health probes fail because JODConverter returns 404 on all health endpoints, and LibreOffice takes 60-90 seconds to initialize. Symptoms:
  • Pod restarts continuously (often 400+ times)
  • Events show: Readiness probe failed: HTTP probe failed with statuscode: 404
  • Logs show: Office process died with exit code 81; restarting it
Solution: The converter must use TCP socket probes, not HTTP probes. Ensure your 25-converter.yaml uses:
livenessProbe:
  tcpSocket:
    port: 8080
  initialDelaySeconds: 120
  periodSeconds: 30
  timeoutSeconds: 10
  failureThreshold: 3
readinessProbe:
  tcpSocket:
    port: 8080
  initialDelaySeconds: 90
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 6
Do NOT use httpGet probes for the converter. The JODConverter REST API returns 404 on /, /health, and /actuator/health endpoints, causing probe failures and endless restart loops.
Why TCP probes work: TCP probes simply verify port 8080 is listening, which it is once Tomcat starts—even while LibreOffice is still initializing. Note: During normal startup, you will see “Office process died with exit code 81” messages in the logs. This is expected behavior while LibreOffice initializes.

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 pod
kubectl exec -n draftable deployment/web -- env | grep S3

# Restart a deployment
kubectl rollout restart deployment/web -n draftable

Production Recommendations

Using Managed AWS Services

For production deployments, replace in-cluster stateful services with managed AWS services:

Amazon RDS for PostgreSQL

  1. Create RDS PostgreSQL instance (Engine: PostgreSQL 16, Multi-AZ enabled)
  2. Update ConfigMap:
    DB_HOST: "your-rds-endpoint.region.rds.amazonaws.com"
    DB_PORT: "5432"
    
  3. Skip applying 10-postgresql.yaml

Amazon ElastiCache for Redis

  1. Create ElastiCache Redis cluster (Engine: Redis 7.x)
  2. Update ConfigMap:
    REDIS_HOST: "your-elasticache-endpoint.region.cache.amazonaws.com"
    REDIS_PORT: "6379"
    
  3. Skip applying 12-redis.yaml

Amazon MQ for RabbitMQ

  1. Create Amazon MQ RabbitMQ broker
  2. Update ConfigMap:
    AMQP_HOST: "your-amazonmq-endpoint.mq.region.amazonaws.com"
    AMQP_PORT: "5671"  # Note: Amazon MQ uses TLS
    
  3. Skip applying 11-rabbitmq.yaml

Configuration Reference

S3 Environment Variables

VariablePurposeRequired
FILE_STORAGE_TYPEMust be s3 to enable S3 storage
S3_STORAGE_BUCKETS3 bucket name
AWS_S3_REGION_NAMEAWS region (note: not AWS_REGION)

Application Environment Variables

For a complete list of environment variables, see the Docker Compose Guide.

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 (sanitized)
  • Error messages from the browser console