Skip to content

Nexadata Production Deployment

Use this guide when Nexadata must run across multiple containers, survive a container failure, or process a large workflow volume. It covers a separate worker pool, DNS-based clustering, load balancing, and an AWS ECS reference implementation. The same pattern works on any container platform.

In a production deployment, it is recommended to run multiple nodes. Application nodes and worker nodes both use the same Docker image, but operate in different modes depending on configuration. Application nodes host the web interface and should be operated behind a load balancer. Worker nodes run in the background, polling for and executing workflows from a persistent queue.

Each node needs:

  • 1 vCPU
  • 4 GB RAM
  • 20 GB storage

For provisioning, we recommend:

  • 2 or more application nodes, ideally behind a load balancer
  • 2 or more worker nodes

When running multiple nodes, clustering is required. See Clustering for how to set up a cluster so the nodes are aware of each other.

You also need the following infrastructure:

RequirementDetails
Container platformAny platform that can run the Nexadata image and supply secrets
PostgresManaged Postgres recommended; connection limit must cover all application and worker containers
S3-compatible storageAWS S3 or another S3-compatible service
SMTP endpointRequired for platform email
Load balancerHTTP health checks and TLS termination
DNS service discoveryA DNS name that resolves to every application and worker container
SecretsSECRET_KEY_BASE, VAULT_KEY, RELEASE_COOKIE, database credentials, storage keys, SMTP credentials, LLM keys

For a single container, see Nexadata Self-Hosted Quickstart.

┌────────────────────────────┐
users ── TLS ───▶ │ load balancer │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ Nexadata application pool │
│ 2 or more containers │
│ web endpoint, port 4000 │
└─────────────┬──────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌────────▼────────┐ ┌─────────▼────────┐ ┌─────────▼────────┐
│ Postgres │ │ S3-compatible │ │ SMTP endpoint │
│ data and jobs │ │ object storage │ │ email delivery │
└─────────────────┘ └──────────────────┘ └──────────────────┘
┌────────────────────────────┐
│ Nexadata worker pool │
│ separate containers │
│ background jobs only │
└────────────────────────────┘

Application containers serve HTTP and do not process background jobs. Worker containers process jobs and do not serve HTTP. Both use the same image, database, storage, and SMTP settings.

SettingApplication containerWorker container
BACKGROUND_WORKERfalsetrue
INLINE_WORKERfalse (default)not used
Exposed port4000none
Health checkGET /healthplatform/process check only
Load balanceryesno
Scaling signalrequests or CPUqueue depth

Both roles use the same NEXADATA_IMAGE, the same RELEASE_COOKIE, and the same database and storage configuration. Every container applies database migrations before starting. Postgres serializes concurrent migrations, so rolling deploys are safe.

After the application service is running, create the initial organization and admin user once. The bootstrap script is included with the Nexadata container and it can be executed on the container:

Terminal window
./bootstrap \
--email admin@example.com \
--password 'ChangeMe123!' \
--org 'Your Organization'

Run this inside one application container (or as a one-off task with the same image and environment). The command creates the database and loads the schema when needed, applies migrations, and is idempotent; it does not reset an existing admin password.

The configuration in Nexadata Self-Hosted Quickstart applies, with these changes:

  • Set INLINE_WORKER=false (or leave it unset).
  • Run application containers with BACKGROUND_WORKER=false.
  • Run worker containers with BACKGROUND_WORKER=true.
  • Enable DNS clustering for every application and worker container.
  • Use a managed Postgres and S3 bucket; do not run database or object storage inside the application containers.

Two variables of particular importance are AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. When not running the application on AWS, these variables are still required, as they serve as keys for most S3-compatible storage options that you would run yourself (i.e. MinIO). These variables are required unless you are running on AWS and use instance role authentication.

VariableRequiredDescription
PUBLIC_URLyesPublic base URL for the application, including port (if not 80 or 443)
SECRET_KEY_BASEyesRandom session signing key; openssl rand -base64 64
VAULT_KEYyesEncryption key for secrets stored in DB, exactly 32 bytes; openssl rand -hex 16
RELEASE_COOKIEyesRandom value to verify clustered nodes can connect to one another
DATABASE_URLyesPostgres connection URL, preferably with sslmode=require
FILE_MANAGER_BUCKETyesPrivate bucket for uploads and workflow outputs
STATIC_FILES_BUCKETfor profile imagesPublicly readable bucket
AWS_REGIONyesStorage region, relevant for S3-compatible storage even if not on AWS
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYyes, with caveat (see above)Omit when using an instance or task role
S3_ENDPOINTfor non-AWS storageS3-compatible endpoint
S3_FORCE_PATH_STYLEfor non-AWS storageUsually true
MAILER_ADAPTERyessmtp
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORDyesSMTP endpoint
SMTP_TLS, SMTP_AUTH, SMTP_SSLnoMatch your SMTP provider
MAILER_FROM_EMAIL, MAILER_FROM_NAMEnoDefault sender
APP_NAME, BUSINESS_NAMEnoSet both to Nexadata
POOL_SIZEnoDatabase connections per container; default 10
PORTnoInternal HTTP port (port inside Docker container); default 4000
VariableRequiredDescription
CLUSTER_STRATEGYyesdns
DNS_POLL_QUERYyesDNS name that resolves to every application and worker container

The Nexadata application relies upon a multi-node strategy where each node is aware of the others, forming a cluster. Nodes will be automatically discovered if using the dns cluster strategy.

The DNS_POLL_QUERY represents a hostname that the application should query to return a list of node IPs. From there, the application will attempt to connect all of the nodes together. This also requires certain ports to be opened, and the RELEASE_COOKIE environment variable to be the same across all nodes. Allow container-to-container traffic on the following ports:

  • port 4369 for node discovery;
  • ports 9000-9010 for node communication;
  • port 9090 for remote calls.
VariableDescription
QUEUE_METRICS=cloudwatchPublishes queue depth to AWS CloudWatch for worker autoscaling
QUEUE_METRICS=sinkLogs queue depth instead; use when your platform scales workers another way

Queue metrics are produced by worker containers. Application containers should use sink. If you are deployed to AWS, this will report the depth of the queue to CloudWatch, which could become the basis for an autoscaling strategy for worker nodes.

VariableDescription
LLM_PROVIDERanthropic (requires LLM_API_KEY) or bedrock (AWS credentials)
LLM_API_KEY, LLM_MODEL, ADVANCED_LLM_MODEL, LLM_BASE_URLAI provider settings
OCR_PROVIDERnone, or textract with AWS S3 and Textract permissions
OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector endpoint; leave unset to disable telemetry
OTEL_EXPORTER_OTLP_HEADERSOTLP vendor headers
CSP_EXTRA_SOURCESComma-separated additional Content Security Policy sources, added to default-src and img-src
SAML variablesSAML_SP_ID, SAML_SP_ENTITY_ID, SAML_SP_CERT/SAML_SP_KEY (PEM contents), SAML_SP_CERTFILE/SAML_SP_KEYFILE (paths), SAML_ORG_*, SAML_CONTACT_*
OAuth variablesHUBSPOT_CLIENT_ID/_SECRET

Nexadata exports traces and metrics over OTLP when an endpoint is configured. Configure both the application and worker containers:

VariableRequiredDescription
OTEL_EXPORTER_OTLP_ENDPOINTyesCollector base URL, without /v1/traces or /v1/metrics
OTEL_EXPORTER_OTLP_PROTOCOLyeshttp/protobuf, or grpc if your collector supports it
OTEL_SERVICE_NAMErecommendedService name shown by your telemetry backend
OTEL_EXPORTER_OTLP_HEADERSif requiredComma-separated authentication headers, for example x-api-key=...
OTEL_RESOURCE_ATTRIBUTESoptionalAdditional resource attributes, for example deployment.environment=production

The exporter appends the signal paths to OTEL_EXPORTER_OTLP_ENDPOINT. Do not set OTEL_SDK_DISABLED=true, because that disables the SDK. The metrics exporter is enabled by the application; no separate metrics flag is required.

The Nexadata image does not include a SAML service-provider certificate or private key. Provide them at runtime and point the application at the files:

SAML_SP_CERTFILE=/run/secrets/saml_sp_cert
SAML_SP_KEYFILE=/run/secrets/saml_sp_key
SAML_SP_ENTITY_ID=https://app.example.com

With Docker Compose, mount them as read-only secrets:

secrets:
saml_sp_cert:
file: ./secrets/saml-sp.crt
saml_sp_key:
file: ./secrets/saml-sp.key
services:
nexadata:
secrets:
- saml_sp_cert
- saml_sp_key

With docker run, mount the files directly:

Terminal window
-v "$PWD/secrets/saml-sp.crt:/run/secrets/saml_sp_cert:ro" \
-v "$PWD/secrets/saml-sp.key:/run/secrets/saml_sp_key:ro"

The files must be readable by the container user (UID 61000 in the supplied image). If SAML is not used, leave both paths unset.

You may also inject them as environment variables: SAML_SP_KEY and SAML_SP_CERT.

If you inject them this way, at startup the container writes them to /tmp/nexadata-secrets with owner-only permissions and sets SAML_SP_CERTFILE / SAML_SP_KEYFILE automatically. Do not also set the file-path variables.

  • Health check: GET /health, expecting HTTP 200.
  • Terminate TLS at the load balancer and forward to container port 4000 (or the exposed port provided by the PORT environment variable).
  • Redirect HTTP to HTTPS, and set PUBLIC_URL to the public HTTPS origin so generated links use the correct scheme and port.
  • Enable sticky sessions so a browser stays on one application container.
  • Set the idle timeout to at least 120 seconds for browser WebSocket connections.
  • Use a deregistration delay of at least 30 seconds for nodes behind a load balancer.
  • Do not route web traffic to worker containers (if using a load balancer, do not register them with the load balancer).

Workflow files are written to container-local disk before upload to S3. With clustering enabled, downloads can read the file from the container that wrote it. If you run multiple application containers without clustering, mount shared storage (for example EFS or NFS) at /tmp on every application and worker container.

AWS ECS/Fargate is one implementation of this topology. The same environment and port rules apply to Kubernetes or another orchestrator.

Run one task definition for the application and one for workers, both with the same image and common environment. Use awsvpc networking and give both task roles S3, SMTP/SES, CloudWatch, and any LLM/Textract permissions you enable.

Application container:

{
"name": "nexadata",
"image": "<nexadata-image>",
"portMappings": [{ "containerPort": 4000, "protocol": "tcp" }],
"environment": [
{ "name": "BACKGROUND_WORKER", "value": "false" },
{ "name": "INLINE_WORKER", "value": "false" },
{ "name": "CLUSTER_STRATEGY", "value": "dns" },
{ "name": "DNS_POLL_QUERY", "value": "nexadata.local" },
{ "name": "PUBLIC_URL", "value": "https://app.example.com" },
{ "name": "FILE_MANAGER_BUCKET", "value": "nexadata-files" },
{ "name": "STATIC_FILES_BUCKET", "value": "nexadata-static" },
{ "name": "QUEUE_METRICS", "value": "sink" }
],
"secrets": [
{ "name": "SECRET_KEY_BASE", "valueFrom": "<secret-arn>" },
{ "name": "VAULT_KEY", "valueFrom": "<secret-arn>" },
{ "name": "RELEASE_COOKIE", "valueFrom": "<secret-arn>" },
{ "name": "DATABASE_URL", "valueFrom": "<secret-arn>" },
{ "name": "SMTP_PASSWORD", "valueFrom": "<secret-arn>" }
]
}

Worker container: identical, but:

{
"environment": [
{ "name": "BACKGROUND_WORKER", "value": "true" },
{ "name": "CLUSTER_STRATEGY", "value": "dns" },
{ "name": "DNS_POLL_QUERY", "value": "nexadata.local" },
{ "name": "QUEUE_METRICS", "value": "cloudwatch" }
]
}

The worker has no portMappings and is not attached to the load balancer.

  • Put both services in the same Cloud Map namespace and make DNS_POLL_QUERY resolve to all task IPs.
  • Attach only the application service to the ALB target group.
  • Allow the task security group to reach itself on 4369, 9000-9010, and 9090.
  • Use RDS for Postgres and S3 for storage.
  • Use secrets for SECRET_KEY_BASE, VAULT_KEY, RELEASE_COOKIE, DATABASE_URL, and SMTP/LLM credentials.
  • Keep the same RELEASE_COOKIE in both task definitions.

With QUEUE_METRICS=cloudwatch, workers publish queue depth to CloudWatch:

  • Namespace: Workflows/QueueDepth
  • Metric: Depth
  • Dimension: QueueName=default

Register the worker ECS service as a scalable target and add a target-tracking policy:

Terminal window
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/nexadata-prod/nexadata-prod-worker \
--min-capacity 1 \
--max-capacity 20
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/nexadata-prod/nexadata-prod-worker \
--policy-name nexadata-worker-queue-depth \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 10,
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60,
"CustomizedMetricSpecification": {
"MetricName": "Depth",
"Namespace": "Workflows/QueueDepth",
"Statistic": "Maximum",
"Dimensions": [{"Name": "QueueName", "Value": "default"}]
}
}'

The task role needs cloudwatch:PutMetricData. Scale application containers from request count or CPU, not queue depth.

  1. Back up Postgres and the S3 buckets.
  2. Deploy the new image to workers, then application containers.
  3. Keep SECRET_KEY_BASE, VAULT_KEY, and RELEASE_COOKIE unchanged.
  4. Verify /health and run a test workflow.

Every container applies migrations before starting. No separate migration step is required; Postgres serializes concurrent migrations during a rolling deploy.

  • Back up Postgres with your provider’s automated backups or pg_dump.
  • Enable versioning or replicate the file-manager and static buckets.
  • Back up the secrets store, especially VAULT_KEY.
SymptomCheck
Container exits at startupLogs contain a configuration error naming every invalid variable
RELEASE_COOKIE is not setSet the same cookie on every container
Containers do not clusterCLUSTER_STRATEGY, DNS_POLL_QUERY, cookie, DNS resolution, ports 4369/9000-9010/9090
Workflows stay queuedWorker containers are running with BACKGROUND_WORKER=true and can reach Postgres
No worker autoscaling metricsQUEUE_METRICS=cloudwatch, cloudwatch:PutMetricData, worker logs
Downloads fail after scalingClustering is enabled, or shared /tmp is mounted on every container
OCR failsTextract requires real AWS S3 and AWS permissions; use OCR_PROVIDER=none otherwise
Email does not arriveSMTP host, port, credentials, TLS, and sender address