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:
- An RDS PostgreSQL database stores users, chats, and settings.
BETTER_AUTH_SECRETandBETTER_AUTH_URLmust be set correctly or sign-in will not work.- The container listens on port 5005.
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
- In the RDS console, Create database -> PostgreSQL. Any supported major version works; pick the latest.
- Choose the Free tier or Dev/Test template for a small team, Production if you want Multi-AZ. A
db.t4g.microordb.t4g.smallis plenty to start. - Set a master username and password. Store the password - you will need it for
DB_URI. - Under Connectivity, place the database in the same VPC as the compute you are about to create, and set Public access to No.
- 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 port5432from the compute’s security group. - Under Additional configuration, set an Initial database name, for example
nao.
Enable SSL
Recent RDS PostgreSQL versions ship withrds.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:
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 KubernetesSecret), 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:
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 tossh in and look.
1.1 Launch the instance
- In EC2, Launch instance with Ubuntu Server 24.04 LTS.
- Instance type:
t3.medium(2 vCPU, 4 GB) is the minimum that runs comfortably;t3.largeif you expect more than a handful of concurrent users. - Storage: 30 GB gp3.
- Network: the same VPC as the RDS instance. Create a security group (for example
nao-ec2-sg) that allows inbound22from your IP, and80and443from anywhere. - 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 groupnao-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:
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.
=== 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 DNSA record for nao.your-company.com at the Elastic IP first, then:
/etc/caddy/Caddyfile:
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
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_URInao/BETTER_AUTH_SECRETnao/OPENAI_API_KEYnao/NAO_CONTEXT_GIT_SSH_KEY(ornao/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
AmazonECSTaskExecutionRolePolicyand 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
- ECS -> Clusters -> Create cluster, Fargate only, in the same VPC as RDS.
- EC2 -> Load balancers -> Create Application Load Balancer, internet-facing, in the public subnets of that VPC. Create a security group
nao-alb-sgallowing inbound80and443from anywhere. - Add an HTTPS:443 listener with a certificate from ACM for
nao.your-company.com, and an HTTP:80 listener that redirects to HTTPS. - Create a target group of type IP, protocol HTTP, port
5005, health check path/. The HTTPS listener forwards to it. - Create a security group
nao-ecs-sgfor the tasks, allowing inbound5005fromnao-alb-sgonly.
2.4 Open the database to the tasks
Onnao-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: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
- In the cluster, Create service: launch type Fargate, the
naotask definition, desired tasks1. - 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. - Load balancing: attach the ALB and the target group from step 2.3. Set the health check grace period to
120seconds so the task has time to clone the context and run migrations before the ALB starts judging it. - Create the service and wait for the task to reach
RUNNINGand the target to becomehealthy.
2.7 Point the domain at the ALB
In Route 53 (or your DNS provider), create an aliasA 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 tracklatest) 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.
Option 3: Kubernetes / EKS
If you already run an EKS cluster, nao is a singleDeployment 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
Onnao-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
--from-literal per warehouse credential your nao_config.yaml reads from the environment.
3.3 Deploy
nao.yaml:
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
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
Scalingreplicas above 1 works, with two conditions:
NAO_STORAGE_BACKENDmust bes3(orlocalon aReadWriteManyvolume such as EFS). See Permanent Storage.BETTER_AUTH_SECRETmust be the same across pods, which it is as long as it comes from the sharedSecret.
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