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 EKS with S3 storage is the fully supported Kubernetes deployment for Draftable API Self-Hosted v3. S3 object storage is the architecture this product is designed around, and this is the Kubernetes path we test, optimise for, and support.Other platforms are possible but carry trade-offs:
  • Azure AKS — functional, but Draftable has no Azure Blob Storage backend, so it requires a shared network file share instead of object storage. See the Azure AKS Deployment Guide, which documents the measured performance implications and the support boundaries before you commit to it.
  • Any other Kubernetes distribution — see the generic Kubernetes Deployment Guide.
See FILE_STORAGE_TYPE for the supported storage backends.
NFS and network file share storage is not supported. Draftable does not test or optimise against NFS, SMB, or any other shared network filesystem, and we cannot provide support for issues arising from network file share storage — including performance, file locking, and mount behaviour.This applies wherever such storage is used, including Azure Files on AKS. If you deploy on Kubernetes and want a supported storage configuration, use AWS EKS with S3.
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.

Prerequisites

Architecture

AWS Setup

EKS Cluster

S3 & IAM

Deploy Draftable

DNS & HTTPS

Troubleshooting


Architecture Overview

Component Architecture

Draftable API Self-Hosted consists of several microservices that work together:

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.
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
Redis persistence is especially critical. Redis stores the activated license and custom fonts. If Redis data is lost, all users will be locked out until the license is reactivated by re-running the web-init job. Always use persistent storage for Redis — or preferably, a managed Redis service with persistence enabled.

Scaling and high availability

The application components are designed to be scaled horizontally, with one exception. The table below shows what each component supports.
Celery Beat must run as exactly one replica. Beat holds its own schedule and there is no leader election between instances, so a second Beat pod fires every scheduled job a second time. One of those jobs moves daily usage tracking data out of Redis into the database and then clears the Redis keys, so two copies running at once can race each other and produce duplicated or dropped usage records.Set replicas: 1 and use strategy: Recreate rather than the default rolling update, so an upgrade never briefly runs two Beat pods at once.Beat is a scheduler, not part of the comparison request path. If its node or Availability Zone is lost, Kubernetes reschedules it into a surviving zone and comparisons continue running normally in the meantime.

Spreading replicas across Availability Zones

For the components that do scale, set the replica count to 2 or more and add a topology spread constraint on the zone label so Kubernetes actively places replicas in different Availability Zones rather than leaving placement to chance:
Add a PodDisruptionBudget with minAvailable: 1 for each scaled component so a node drain cannot remove the last running replica. Comparison jobs are queued durably and are only acknowledged once they complete, so a comparison interrupted by a pod eviction or a zone failure returns to the queue and is picked up by a surviving Compare replica rather than being lost. Compare and Converter are the memory-hungry components, so doubling their replica count roughly doubles their share of your node capacity. Size your node group accordingly.
Do not make the in-cluster stateful components highly available by raising their replica count. The 10-postgresql.yaml, 11-rabbitmq.yaml and 12-redis.yaml manifests each run a single pod against a ReadWriteOnce PersistentVolumeClaim. Two PostgreSQL or Redis pods cannot share that volume, and an EBS-backed ReadWriteOnce volume is pinned to a single Availability Zone, so the pod that mounts it cannot move between zones.RabbitMQ is the most damaging of the three to get wrong. Two RabbitMQ pods behind one Service that are not clustered together become two independent brokers. Publishers and consumers can then connect to different brokers, and comparisons will sit in progress indefinitely without producing an error.For multi-zone resilience, use the managed services in the table above: Amazon RDS and Amazon ElastiCache both support Multi-AZ with automatic failover, and Amazon MQ for RabbitMQ offers an active/standby multi-AZ deployment. If you must self-manage RabbitMQ, it needs a genuine cluster with peer discovery and quorum queues, not a raised replica count.

S3 Authentication Architecture

Critical: The Compare service does NOT support IRSA (IAM Roles for Service Accounts). It requires explicit AWS credentials.
The Compare service (.NET application) is missing the AWSSDK.SecurityToken assembly required for AssumeRoleWithWebIdentity. Attempting to use IRSA will result in:

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: Verify installations:

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


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


Create EKS Cluster

Cluster Configuration

Download or create the cluster configuration file:

eksctl-cluster-config.yaml

EKS cluster configuration for Draftable
Key configuration points:
Kubernetes Version: Always use the latest stable version. Check available versions with:

Create the Cluster

This command takes 15–25 minutes to complete. Do not interrupt the process.

Configure kubectl

After cluster creation, configure kubectl to connect:
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

You should see 3 nodes in Ready state.

Create S3 Bucket

Create the Bucket

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:
Apply the CORS configuration:

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

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:

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:

Create IAM User for Compare Service

Required: The Compare service cannot use IRSA and must have explicit AWS credentials.
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

Create Service Account

Install Controller via Helm

Verify the controller is running:

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.

Update Configuration Files

Before deploying, update these files with your values:

02-configmap.yaml

Set DB_CONN_MAX_AGE to "0". With any other value, including the default, the web pods leave idle database connections open after requests until PostgreSQL refuses new ones and the API returns HTTP 500 with FATAL: sorry, too many clients already. See DB_CONN_MAX_AGE in the Docker Compose Guide.
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

30-ingress.yaml

Create Secrets

1

Create namespace

2

Create application secrets

3

Create AWS credentials for Compare service

Deploy Infrastructure

1

Apply configuration

2

Deploy infrastructure services

3

Wait for infrastructure pods

Run Database Migrations

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


Configure DNS and HTTPS

Get ALB DNS Name

The ADDRESS column shows the ALB DNS name.

Create DNS Record

In your DNS provider, create a CNAME record:

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

All pods should show Running status with 1/1 or 2/2 ready. Expected output:

Check S3 Connectivity

After creating a comparison, verify files are stored in S3:

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

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

Viewer Doesn’t Render Documents (CORS Error)

Cause: S3 bucket CORS not configured. Solution: Apply CORS configuration:

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


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:
  3. Skip applying 10-postgresql.yaml

Amazon ElastiCache for Redis

  1. Create ElastiCache Redis cluster (Engine: Redis 7.x)
  2. Update ConfigMap:
  3. Skip applying 12-redis.yaml

Amazon MQ for RabbitMQ

Amazon MQ must use the RabbitMQ engine. Amazon MQ for ActiveMQ is not compatible with Draftable (it uses AMQP 1.0; Draftable requires AMQP 0-9-1), and AWS SQS is not supported. See supported message brokers. If your RabbitMQ or Redis uses a private or internal CA, see Using a private or internal CA.
  1. Create Amazon MQ RabbitMQ broker
  2. Update ConfigMap:
  3. Skip applying 11-rabbitmq.yaml

Configuration Reference

S3 Environment Variables

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