Skip to main content
This page walks through three ways of running the getnao/nao container on AWS. Pick the one that matches how your team already runs things: Whatever option you choose, three things are the same:
  1. An RDS PostgreSQL database stores users, chats, and settings.
  2. BETTER_AUTH_SECRET and BETTER_AUTH_URL must be set correctly or sign-in will not work.
  3. The container listens on port 5005.
They are covered first, then each option in turn. This page assumes you have already gone through Step 1 and 2 of the Deployment Guide: a nao context repository with secrets replaced by environment variables, and either a Dockerfile that bakes it into the image or a Git URL the container can clone at startup.

Before you start: the RDS database

nao needs a PostgreSQL database for everything that is not context: user accounts, sessions, chats, automations, and settings.

Create the instance

  1. In the RDS console, Create database -> PostgreSQL. Any supported major version works; pick the latest.
  2. Choose the Free tier or Dev/Test template for a small team, Production if you want Multi-AZ. A db.t4g.micro or db.t4g.small is plenty to start.
  3. Set a master username and password. Store the password - you will need it for DB_URI.
  4. Under Connectivity, place the database in the same VPC as the compute you are about to create, and set Public access to No.
  5. Create a dedicated security group for the database (for example nao-rds-sg). You will add an inbound rule to it in each option below, allowing port 5432 from the compute’s security group.
  6. Under Additional configuration, set an Initial database name, for example nao.
Once the instance is available, copy its endpoint from the Connectivity & security tab. Your connection string is:

Enable SSL

Recent RDS PostgreSQL versions ship with rds.force_ssl=1 in the default parameter group, so unencrypted connections are refused. Set DB_SSL=true on the container so nao connects over TLS:
nao runs its migrations automatically on startup, so an empty database is all you need.
The RDS instance must be reachable from the container on port 5432. On AWS this is a security-group question, not a credentials question: if nao logs connection timed out at startup, the inbound rule on the RDS security group is missing. Each option below tells you which security group to allow.

Before you start: the environment variables

These variables are required on every option. Everything sensitive should be a secret (Secrets Manager, SSM Parameter Store, or a Kubernetes Secret), never a plain-text value in a task definition or manifest committed to Git.

BETTER_AUTH_SECRET

A long random string used to sign authentication sessions. Generate it once:
This is critical. If BETTER_AUTH_SECRET is not set, the container generates a new one every time it starts. On ECS or Kubernetes that means every deploy, every scale event, and every task replacement signs out all your users. Generate the value once, store it as a secret, and reuse it across every revision of the deployment.

BETTER_AUTH_URL

The URL your users type in their browser, scheme included and without a trailing path. It is used to build sign-in callbacks, invitation and password-reset links, and to validate the origin of requests, so it must match exactly what is in front of the container:
  • If you put an ALB or reverse proxy with TLS in front, this is https:// even though the container itself speaks plain HTTP on 5005.
  • If you do not have a domain yet, use the load balancer’s DNS name (http://nao-alb-123456.eu-west-1.elb.amazonaws.com) or the instance’s public IP, and update the variable when you map a domain. A wrong value here typically shows up as a redirect loop or an “invalid origin” error after sign-in.
  • Google, GitHub, or SAML sign-in must use the same URL as their redirect URI base. See Admin setup.

Option 1: EC2 with Docker

The most direct option: one virtual machine, Docker installed by hand, the container started with a compose file. Good for a first deployment and for teams that want to be able to ssh in and look.

1.1 Launch the instance

  1. In EC2, Launch instance with Ubuntu Server 24.04 LTS.
  2. Instance type: t3.medium (2 vCPU, 4 GB) is the minimum that runs comfortably; t3.large if you expect more than a handful of concurrent users.
  3. Storage: 30 GB gp3.
  4. Network: the same VPC as the RDS instance. Create a security group (for example nao-ec2-sg) that allows inbound 22 from your IP, and 80 and 443 from anywhere.
  5. Attach a key pair, launch, and note the public IP. Allocate an Elastic IP and associate it so the address survives a stop/start.

1.2 Open the database to the instance

On the RDS security group nao-rds-sg, add an inbound rule: Referencing the instance’s security group rather than an IP means the rule keeps working if you replace the instance.

1.3 Install Docker

1.4 Configure and start nao

Create a working directory and an .env file. This file holds your secrets, so keep it readable by your user only.
.env:
docker-compose.yml:
The port is bound to 127.0.0.1 on purpose: the reverse proxy in the next step is the only thing that should talk to the container from outside.
You should see === Starting Services === followed by the backend listening on 5005. curl -I http://127.0.0.1:5005 returns a 200.
If you built your own image with the context copied in (the Dockerfile from the Deployment Guide), replace image: getnao/nao:latest with your image and drop the NAO_CONTEXT_* variables in favour of NAO_DEFAULT_PROJECT_PATH=/app/project.

1.5 Put HTTPS in front

The simplest way to terminate TLS on a single box is Caddy, which obtains and renews a Let’s Encrypt certificate automatically. Point a DNS A record for nao.your-company.com at the Elastic IP first, then:
/etc/caddy/Caddyfile:
Open https://nao.your-company.com, confirm the chat UI loads, and complete the first sign-up. Make sure BETTER_AUTH_URL in .env matches this domain; if you changed it, run docker compose up -d again to restart with the new value. If you prefer to terminate TLS on AWS rather than on the box, put an Application Load Balancer with an ACM certificate in front of the instance instead, forward to port 5005, and restrict nao-ec2-sg so that 5005 is only reachable from the ALB’s security group.

1.6 Update nao

Sessions survive the restart because BETTER_AUTH_SECRET is fixed in .env, and data survives because it lives in RDS.

Option 2: ECS on Fargate

Fargate runs the container without any instance to manage - the closest AWS equivalent to Cloud Run. You describe the container in a task definition, an ECS service keeps the desired number of tasks running, and an Application Load Balancer exposes it over HTTPS.
AWS App Runner is an even more Cloud Run-like service and does run the getnao/nao image, but it cannot attach a VPC security group to reach a private RDS instance without a VPC connector, and it offers less control over health checks and timeouts. ECS on Fargate is the recommended path.

2.1 Store the secrets

Create one secret per sensitive value in AWS Secrets Manager (plain-text secrets, not key/value JSON), and note each ARN:
  • nao/DB_URI
  • nao/BETTER_AUTH_SECRET
  • nao/OPENAI_API_KEY
  • nao/NAO_CONTEXT_GIT_SSH_KEY (or nao/NAO_CONTEXT_GIT_TOKEN)
  • one per warehouse credential referenced in nao_config.yaml

2.2 Create the IAM roles

Two roles are involved:
  • Task execution role - used by ECS itself to pull the image and inject secrets. Start from the managed AmazonECSTaskExecutionRolePolicy and add permission to read your secrets:
  • Task role - used by nao at runtime. It needs nothing for a basic deployment. Grant it S3 permissions if you use the S3 storage backend, which is the right choice on Fargate since task filesystems are ephemeral.

2.3 Create the cluster and the load balancer

  1. ECS -> Clusters -> Create cluster, Fargate only, in the same VPC as RDS.
  2. EC2 -> Load balancers -> Create Application Load Balancer, internet-facing, in the public subnets of that VPC. Create a security group nao-alb-sg allowing inbound 80 and 443 from anywhere.
  3. Add an HTTPS:443 listener with a certificate from ACM for nao.your-company.com, and an HTTP:80 listener that redirects to HTTPS.
  4. Create a target group of type IP, protocol HTTP, port 5005, health check path /. The HTTPS listener forwards to it.
  5. Create a security group nao-ecs-sg for the tasks, allowing inbound 5005 from nao-alb-sg only.

2.4 Open the database to the tasks

On nao-rds-sg, add an inbound rule:

2.5 Write the task definition

Register this with ECS -> Task definitions -> Create new task definition with JSON, replacing account ID, region, and ARNs:
1 vCPU and 4 GB is a sensible starting size.
To use your own image instead of cloning at startup, push the image built from the Deployment Guide’s Dockerfile to Amazon ECR, reference it in "image", and replace the NAO_CONTEXT_* variables with { "name": "NAO_DEFAULT_PROJECT_PATH", "value": "/app/project" }.

2.6 Create the service

  1. In the cluster, Create service: launch type Fargate, the nao task definition, desired tasks 1.
  2. Networking: the VPC’s private subnets, security group nao-ecs-sg. If the private subnets have no NAT gateway, enable public IP so the task can reach the LLM API, Git, and Docker Hub - or add a NAT gateway.
  3. Load balancing: attach the ALB and the target group from step 2.3. Set the health check grace period to 120 seconds so the task has time to clone the context and run migrations before the ALB starts judging it.
  4. Create the service and wait for the task to reach RUNNING and the target to become healthy.

2.7 Point the domain at the ALB

In Route 53 (or your DNS provider), create an alias A record for nao.your-company.com pointing at the ALB. Open the URL, confirm the chat UI loads, and complete the first sign-up. If you deployed before having a domain, BETTER_AUTH_URL should be the ALB DNS name over http:// for now. Update it to the final https:// domain and register a new task definition revision - the service will roll the task.

2.8 Update nao

Register a new revision of the task definition (or simply Update service -> Force new deployment if you track latest) and ECS replaces the task. Because BETTER_AUTH_SECRET comes from Secrets Manager and data lives in RDS, users stay signed in through the rollout.
Do not scale the service above one task without switching NAO_STORAGE_BACKEND to s3. With the default local backend each task has its own filesystem, so files saved by the agent would appear and disappear depending on which task serves a request. See Permanent Storage.

Option 3: Kubernetes / EKS

If you already run an EKS cluster, nao is a single Deployment plus a Service and an Ingress. The manifests below use the AWS Load Balancer Controller for the ingress, which is the standard way to get an ALB with an ACM certificate on EKS. Any other ingress controller works too; only the annotations change.

3.1 Open the database to the cluster

On nao-rds-sg, add an inbound rule allowing PostgreSQL from the cluster: The cluster security group ID is shown on the cluster’s Networking tab in the EKS console. If your cluster uses security groups for pods, reference the pod security group instead.

3.2 Create the namespace and the secrets

Add one --from-literal per warehouse credential your nao_config.yaml reads from the environment.
Run the BETTER_AUTH_SECRET generation once. If you recreate the secret from a script on every deploy, every rollout signs out all users. Prefer syncing it from AWS Secrets Manager with the External Secrets Operator or the Secrets Store CSI driver so the value has a single home.

3.3 Deploy

nao.yaml:
The startup probe allows three minutes for the container to clone the context and run migrations against RDS. Once the ingress shows an address, create an alias A record for nao.your-company.com pointing at that ALB hostname, open the URL, and complete the first sign-up. The ServiceAccount annotation wires an IAM role for service accounts so nao can reach the S3 storage bucket without static keys; the role needs the four S3 actions on the bucket. Drop the annotation and the NAO_STORAGE_* entries if you want to start with NAO_STORAGE_BACKEND=none.
To run your own image with the context baked in, push it to ECR, set image: accordingly, and replace the NAO_CONTEXT_* entries in the ConfigMap with NAO_DEFAULT_PROJECT_PATH: "/app/project".

3.4 Update nao

or, if you track latest, kubectl -n nao rollout restart deployment/nao. Pin a version tag in production so a rollout is a deliberate action.

Running more than one replica

Scaling replicas above 1 works, with two conditions:
  • NAO_STORAGE_BACKEND must be s3 (or local on a ReadWriteMany volume such as EFS). See Permanent Storage.
  • BETTER_AUTH_SECRET must be the same across pods, which it is as long as it comes from the shared Secret.

Troubleshooting

Next steps

Deployment Guide

Context repository setup, SMTP for email, first sign-up, and post-deploy customisation

Permanent Storage

Configure the S3 backend and IAM permissions for agent file storage

Admin Setup

Invite users and configure Google, GitHub, or SAML sign-in