Open Connector
Self-Hosting

Deploy to AWS

A self-contained guide to the production AWS, Cloudflare, and Neon topology.

Open Connector's production topology is one AWS EC2 host running Docker Compose. Cloudflare Tunnel accepts public traffic for the API and SSR sites; the console SPA is stored in S3 and served through CloudFront; Postgres runs on Neon. Secrets are held in AWS Secrets Manager with a customer-managed KMS key, and non-secret runtime configuration is in SSM Parameter Store.

Fly.io is not a supported deployment path. This topology intentionally exposes no application or SSH ports on the EC2 security group.

What you need

  • AWS CLI credentials for the production account, Docker with buildx, and OpenTofu 1.6+ (Terraform-compatible tooling also works).
  • A Cloudflare account that owns your domain and a Neon production database URL.
  • A public API hostname, console hostname, marketing hostname, and docs hostname.
  • Values for DATABASE_URL, BETTER_AUTH_SECRET, CONNECTOR_ENCRYPTION_KEY, TUNNEL_TOKEN, and the public origins. Generate the two cryptographic secrets with openssl rand -base64 48.

Production topology

Public hostnameEntryDestinationPurpose
api.example.comCloudflare Tunnelserver:3000API, auth, OAuth callbacks, and broker routes
www.example.comCloudflare Tunnelweb:3000Marketing site
docs.example.comCloudflare Tunnelfumadocs:3000Documentation site
app.example.comCloudflare → CloudFrontprivate S3 bucketConsole SPA

The EC2 host runs server, web, fumadocs, cloudflared, and an on-demand worker. Cloudflare Tunnel and AWS SSM both make outbound connections, so the host needs no inbound security-group rules. Use SSM Session Manager instead of SSH for emergency access.

1. Provision AWS

Create a local terraform.tfvars from the supplied example, set the ARN(s) allowed to administer the KMS key, then provision the stack:

cd deploy/terraform
cp terraform.tfvars.example terraform.tfvars
tofu init
tofu apply

Record these outputs. They are required for the first deployment and for GitHub Actions:

tofu output instance_id
tofu output ecr_registry
tofu output cloudfront_distribution_id
tofu output cloudfront_domain
tofu output github_actions_deploy_role_arn

Provisioning creates the KMS key, ECR repositories, the private S3 bucket and CloudFront distribution for the SPA, the versioned blob bucket, and an arm64 EC2 instance with its least-privilege instance role. The instance is bootstrapped with Docker, Compose, AWS CLI, the production compose file, and the secret-fetch script.

2. Seed runtime configuration and secrets

Create the ignored plaintext input file, fill in the required values, then push it to AWS:

cd ../..
cp deploy/secrets.plain.env.example deploy/secrets.plain.env
AWS_REGION=us-east-1 deploy/scripts/seed-secrets.sh

The deploy-time input file must never be committed. The script writes sensitive values to open-connector/prod/env/<NAME> in Secrets Manager and managed OAuth credentials to open-connector/prod/oauth/<provider> as { "client_id": "…", "client_secret": "…" }. It writes non-secret origins and feature configuration to /open-connector/prod-config/<NAME> in SSM.

At startup, the host materialises those values into /run/open-connector/secrets.env on tmpfs (mode 0600) and Compose passes them to the containers. The durable secret source is Secrets Manager—not the repository, image, or host disk.

3. Configure ingress

In Cloudflare Zero Trust, create a remotely managed Cloudflared tunnel. Put its token in TUNNEL_TOKEN, re-run the secret-seeding command, then add these public hostnames to the tunnel:

HostnameService URL
API hostnamehttp://server:3000
Marketing hostnamehttp://web:3000
Documentation hostnamehttp://fumadocs:3000

For the console, create a proxied CNAME from its hostname to the CloudFront domain output by OpenTofu. CloudFront must have that hostname as an alternate domain name with a validated ACM certificate in us-east-1; otherwise Cloudflare's forwarded host header produces a 403.

4. Deploy

Run from the repository root after exporting the instance and distribution identifiers:

INSTANCE_ID=i-... \
CF_DISTRIBUTION_ID=E... \
deploy/scripts/deploy.sh

The default deploy builds arm64 images, pushes them to ECR, migrates Neon before a server roll, publishes the console to S3 and invalidates CloudFront, then asks SSM to pull and restart the selected services. Image tags are Git SHAs, so a partial deployment leaves unrelated services on their existing image.

Use the smallest service slice that contains your change:

SERVICES=server deploy/scripts/deploy.sh       # API + database migration
SERVICES=fumadocs deploy/scripts/deploy.sh     # documentation SSR only
SERVICES=web deploy/scripts/deploy.sh          # marketing SSR only
SERVICES=app deploy/scripts/deploy.sh          # S3/CloudFront console only
SERVICES=server,worker deploy/scripts/deploy.sh

The same service names are available in the manual Deploy (AWS) GitHub Actions workflow. Configure that workflow with AWS_DEPLOY_ROLE_ARN, AWS_REGION, ECR_REGISTRY, INSTANCE_ID, and CF_DISTRIBUTION_ID.

Verify and operate

After a full deploy, verify the API root, docs, app shell, and marketing sitemap. The deployment script performs matching smoke checks for each selected service.

curl --fail https://api.example.com/
# OK
curl --fail https://docs.example.com/
curl --fail https://www.example.com/sitemap.xml

Use SSM for a shell or a concise runtime inspection:

aws ssm start-session --target "$INSTANCE_ID"

aws ssm send-command --region us-east-1 --instance-ids "$INSTANCE_ID" \
  --document-name AWS-RunShellScript \
  --parameters 'commands=["cd /opt/open-connector && docker compose -f docker-compose.prod.yml ps"]'

To rotate a secret, update the ignored plaintext input file, run seed-secrets.sh again, and recreate the affected container so it fetches the new env-file. To roll back, deploy a previously published Git SHA; tagged ECR images are retained while only untagged images expire.

Troubleshooting

SymptomCheck and resolution
Deployment claims SSM timed outInspect the command invocation and public endpoint before declaring failure; a completed host roll can outlive the waiter.
Invalid environment variables at SSR bootVITE_* values are build-time inputs. Rebuild the affected web/docs/app image; changing runtime secrets alone cannot change them.
Auth returns a database-column errorThe production schema is behind code. Deploy the server slice so its migration stage runs.
A new Cloudflare hostname has no certificateEdge certificate provisioning can take 30–40 minutes. This is Cloudflare propagation, not an origin TLS failure.
Console hostname returns 403 from CloudFrontCheck the CloudFront alternate-domain name and its validated us-east-1 ACM certificate.

On this page