Network Access: Domain or IP
Nginx runs on the host and publishes the stack. Follow Option A if you chose the domain block in .env, or Option B if you chose the bare-IP block.
Nginx reads /etc/nginx/nginx.conf. The files in the repository's nginx/ folder are templates: editing them has no effect until they are copied to /etc/nginx/nginx.conf and Nginx is reloaded.
Install Nginx
sudo apt update
sudo apt install -y nginx
sudo systemctl status nginx
Option A: Domain with HTTPS
Step 1: Create the DNS Records
In the DNS provider that manages the customer's domain (Cloudflare, GoDaddy, Route53, etc.), create two A records:
| Type | Name | Value |
|---|---|---|
| A | onprem.customer-domain.com | SERVER_IP |
| A | default.customer-org-name.customer-domain.com | SERVER_IP |
The second name must match DOMAIN_URL in .env.
Step 2: Verify DNS Resolution
dig +short onprem.customer-domain.com @1.1.1.1
dig +short default.customer-org-name.customer-domain.com @1.1.1.1
Both must return the server's IP before you request certificates.
Step 3: Open the Firewall
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status
Allow port 22 before enabling the firewall, or you will lock yourself out of SSH. Port 80 must be open for Certbot's domain validation.
Step 4: Obtain SSL Certificates
sudo apt install -y certbot python3-certbot-nginx
sudo certbot certonly --nginx -d onprem.customer-domain.com
sudo certbot certonly --nginx -d default.customer-org-name.customer-domain.com
A wildcard certificate for *.customer-domain.com does not cover the tenant name, because it is two labels deep - it needs its own certificate.
Step 5: Install the Nginx Configuration
The template uses the placeholders example.com and <org>. Copy it into place with the customer's domain and organization name substituted - this also updates the CORS origins at the top of the file:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
sed -e 's/<org>/customer-org-name/g' -e 's/example\.com/customer-domain.com/g' nginx/nginx.conf | sudo tee /etc/nginx/nginx.conf > /dev/null
sudo rm -f /etc/nginx/sites-enabled/default
The domain configuration refuses requests made to the bare IP address (return 444). Browse to the domain name.
Step 6: Test and Reload Nginx
sudo nginx -t && sudo systemctl reload nginx
Open https://onprem.customer-domain.com - the sign-in page should load.
Option B: Bare IP over HTTP
Use this when the server is reached by IP address only, for example on a private network over VPN. No DNS records or certificates are needed.
Step 1: Open the Firewall
sudo ufw allow 22
sudo ufw allow 80
sudo ufw enable
Step 2: Install the Nginx Configuration
nginx-ip.conf works for any IP address without editing - it allows any http://<ipv4> origin.
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
sudo cp nginx/nginx-ip.conf /etc/nginx/nginx.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Ubuntu's Nginx package enables a default site that also listens on port 80 as the default server. The AuthNull configuration defines its own, and Nginx allows only one per port, so nginx -t fails with a "duplicate default server" error until that site is removed. sites-enabled/default is a link; the original stays in /etc/nginx/sites-available/.
Step 3: Verify
- Open
http://SERVER_IP/org/signup. The organization sign-up page should load - this confirms Nginx is serving the Admin UI. - Create the organization there, as described in Organization and Tenant Setup. The tenant only exists after this step.
- Open
http://SERVER_IP/tenant/login. You are redirected to the sign-in page, and its address should containdomainUrl=default.customer-org-name.SERVER_IP- the tenant fromDOMAIN_URL, not the bare IP address.
nginx-ip.conf includes a temporary patch (the location ~ ^/static/js/main... block) that makes the Admin UI send DOMAIN_URL instead of the browser's IP. Without it, the sign-in link carries domainUrl=SERVER_IP and the backend cannot find the organization. Leave it in place; it becomes a no-op once the Admin UI image includes the fix.
Restrict the Published Ports
The compose file publishes every container port on all of the server's interfaces, including PostgreSQL (5432), Redis (6379), Cassandra (9042), and the internal services (2882, 6066, 9255, 9291). Docker writes its own firewall rules ahead of ufw, so these ports stay reachable from the network even with the firewall above enabled.
Nginx reaches the containers through 127.0.0.1, so with Nginx in front, bind each mapping in docker-compose.yml to the loopback address - for example change "5432:5432" to "127.0.0.1:5432:5432" - then apply it:
docker compose up -d
Only ports 80 and 443 then remain public. Leave 2882 reachable from any gateway host that calls authn-service directly, such as a ProxySQL server installed on another machine.
Next Steps
Continue to Organization and Tenant Setup to register your organization.