Skip to main content

On-Premises Installation

This guide covers getting the deployment files, configuring the .env file, installing Docker, and starting the Authnull On-Premises stack.

Step 1: Get the Deployment Files​

Clone the deployment repository authnull on the server:

git clone https://github.com/authnull0/authnull.git
cd authnull

The repository contains deployment files only - no application source. Every image is pulled from docker-repo-public.authnull.com.

FilePurpose
docker-compose.ymlThe stack definition
.envConfiguration read by Docker Compose - a blank template
nginx/nginx.confNginx configuration for a domain with HTTPS
nginx/nginx-ip.confNginx configuration for a bare IP address over HTTP
db-init/Database schema and migrations, applied automatically on every start

Step 2: Configure Environment Variables​

Open .env. Every value you must set is marked # [REQUIRED] on the line above it; everything else is preset. To list them:

grep -n -A1 '\[REQUIRED\]' .env
tip

To keep the template untouched, copy it to .env.prod (it is ignored by git), fill that in, and add --env-file .env.prod to every docker compose command.

Database credentials​

These are the credentials for the PostgreSQL container the stack creates. Choose any user and password; each pair must be identical.

DB_USER=authnull
DB_PASSWORD=choose-a-strong-password
POSTGRES_USER=authnull
POSTGRES_PASSWORD=choose-a-strong-password

Organization​

The organization's short name, for example acme. It becomes part of the tenant name, default.<ORG_NAME>.<domain>.

ORG_NAME=customer-org-name

Access mode​

Uncomment one of the two blocks in .env, and replace the placeholders. URLs must not end with a slash.

A) Domain with HTTPS - use with nginx/nginx.conf. Needs DNS records and certificates (see Network Access). Replace example.com with the customer's domain, <org> with ORG_NAME, and <vm-ip> with the server's IP:

SYSTEM_URL=https://onprem.customer-domain.com
SYSTEM_IP=SERVER_IP
DOMAIN_URL=default.customer-org-name.customer-domain.com
SSC_SIGNIN_URL=https://onprem.customer-domain.com/ssc/signin
SSC_BASE_URL=https://onprem.customer-domain.com
CLIENT_URL=https://onprem.customer-domain.com
TENANT_BASE_DOMAIN=customer-domain.com
WEBAUTHN_RP_ID=customer-domain.com
WEBAUTHN_ORIGIN=https://onprem.customer-domain.com,https://default.customer-org-name.customer-domain.com
ROOT_URL=https://onprem.customer-domain.com
SSC_URL=https://onprem.customer-domain.com/ssc/signin
AUTHNULL_LOGOUT_URL=https://onprem.customer-domain.com/ssc/signin

B) Bare IP over HTTP - use with nginx/nginx-ip.conf. No DNS or certificate needed. Replace <vm-ip> with the server's IP and <org> with ORG_NAME:

SYSTEM_URL=http://SERVER_IP
SYSTEM_IP=SERVER_IP
DOMAIN_URL=default.customer-org-name.SERVER_IP
SSC_SIGNIN_URL=http://SERVER_IP/ssc/signin
SSC_BASE_URL=http://SERVER_IP
CLIENT_URL=http://SERVER_IP
TENANT_BASE_DOMAIN=SERVER_IP
WEBAUTHN_RP_ID=SERVER_IP
WEBAUTHN_ORIGIN=http://SERVER_IP
ROOT_URL=http://SERVER_IP
SSC_URL=http://SERVER_IP/ssc/signin
AUTHNULL_LOGOUT_URL=http://SERVER_IP/ssc/signin
note

DOMAIN_URL identifies the tenant: the backend takes its second dot-separated label as the organization's database name, so it must be default.<ORG_NAME>.<domain>, and <ORG_NAME> must match the organization you create in Organization and Tenant Setup.

In bare-IP mode, DOMAIN_URL is a tenant ID, not an address - for example default.acme.10.0.0.5. Users still browse to http://SERVER_IP.

TENANT_BASE_DOMAIN must be the part of DOMAIN_URL after default.<ORG_NAME>.. When the organization is created, its tenant is stored as default.<ORG_NAME>.<TENANT_BASE_DOMAIN>, and sign-in finds the tenant by matching that against DOMAIN_URL - if the two differ, sign-in fails with "Tenant not found or inactive".

SSC_SIGNIN_URL must not be empty, or /tenant/login reloads forever.

note

Passkeys (WebAuthn) need a domain, so they do not work in bare-IP mode. Push MFA and TOTP do.

Per-install secrets​

Generate each value and paste it in:

VariableGenerate with
JWT_SECRETopenssl rand -hex 32
INTERNAL_API_KEYopenssl rand -hex 32
REDIS_PASSWORDopenssl rand -hex 32
TOTP_ENCRYPTION_KEYopenssl rand -hex 32 (64 hex characters)
ENCRYPTION_KEYopenssl rand -hex 16 (exactly 32 hex characters)

Leaving JWT_SECRET empty signs login tokens with a blank key; leaving INTERNAL_API_KEY empty breaks policy lookups and auditing between services.

warning

Do not change ENCRYPTION_KEY or TOTP_ENCRYPTION_KEY after the stack has run. Changing them does not fail loudly - it makes existing encrypted data unreadable and locks every user out of TOTP MFA. Keep a backup of the filled-in .env, and never commit it to git.

Email (SMTP)​

AuthNull sends verification and invitation emails through the customer's mail relay:

SMTP_HOST=smtp.office365.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=sender-password

Sign-in factors​

Set this so the sign-in page offers TOTP and passkeys directly. The mobile wallet is no longer used, and an empty value can send users with older console builds into a wallet-pairing screen:

WALLET_ONBOARDING_REQUIRED=false

Push MFA (Firebase)​

Only needed when an organization's MFA provider is AuthNull's own app, whose pushes go through Firebase Cloud Messaging. Save the Firebase service account file AuthNull provides as secrets/fcm-service-account.json in the repository folder, and set:

FCM_SERVICE_ACCOUNT_JSON=/run/secrets/fcm-service-account.json
warning

If the file is missing when the stack starts, Docker creates an empty directory with that name and push notifications silently stop working. Check with ls -la secrets/. Never commit secrets/ - it is ignored by git.

Optional settings​

Integrations further down in .env (Twilio, Duo, Okta, Azure or AWS storage, push MFA tuning) are disabled while blank. A few worth knowing:

VariableNotes
TWILIO_FROM_NUMBERThe sender number for SMS. TWILIO_PHONE is not read
TRUSTED_PROXIESSet to the Nginx address, or every client IP is logged as the proxy's
DB_MAX_OPEN_CONNS, DB_MAX_IDLE_CONNSPer database. The total, (organizations + 1) × DB_MAX_OPEN_CONNS, must fit within PostgreSQL's max_connections
MFA_PUSH_SWEEP_ENABLEDSet true on exactly one replica
MFA_PUSH_ALLOW_UNSIGNEDEmpty means push signatures are enforced
MFA_PUSH_RISK_ENABLEDLeave false until about a week of activity has built up
ORG_DISCOVERY_ENABLEDEmail-to-organization lookup. Off by default, because it allows account enumeration

Port conflicts​

PAM_HOST_PORT defaults to 9090, which Cockpit uses on many Linux installs. Change it if it collides.

Step 3: Install Docker and Docker Compose​

Connect to the server over SSH, then install Docker and Docker Compose.

1. Update the package index​

sudo apt-get update && sudo apt-get upgrade -y

2. Install prerequisite packages​

sudo apt-get install -y ca-certificates curl

3. Add Docker's official GPG key​

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

4. Add the Docker repository to APT sources​

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

5. Install Docker Engine, CLI, and Compose plugin​

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

6. Start and enable the Docker service​

sudo systemctl enable --now docker
sudo systemctl status docker

7. Allow your user to run Docker without sudo​

sudo usermod -aG docker $USER
newgrp docker

Without this step, Docker commands fail with permission denied while trying to connect to the docker API; prefix them with sudo instead.

8. Verify the installation​

docker --version
docker compose version

Step 4: Start the Stack​

From the authnull directory:

docker compose pull
docker compose up -d

Start-up is ordered by health checks: once postgres and redis are healthy, did-schema-init applies the schema and migrations, then authnull-service starts, then the Admin UI.

warning

Do not add --build. The compose file has a build: section for authnull-service, but its source is not in this repository - always pull the published image.

Step 5: Verify Container Status​

docker compose ps -a

Containers are named onprem-<service>-1, after COMPOSE_PROJECT_NAME in .env. All services should show Up (and healthy where a health check is defined). The two one-off jobs, did-schema-init and cassandra-init, should show Exited (0). If did-schema-init exited with any other code, check it with docker compose logs did-schema-init.

Check the backend directly:

curl -s http://127.0.0.1:8080/system/v1/health

It should return {"status":"ok"}.

Updating the Stack​

Images are published under fixed tags, and a new build is pushed under the same tag. docker compose up reuses an image that is already on the server, so always pull first:

docker compose pull <service>
docker compose up -d <service>

For example, docker compose pull did-react && docker compose up -d did-react updates the Admin UI.

Every docker compose up also replays all database migrations against the master database and every organization's database. If a migration fails, did-schema-init exits with an error and authnull-service does not start - check docker compose logs did-schema-init.

danger

docker compose down stops the stack and keeps its data. docker compose down -v also deletes every data volume - PostgreSQL, Redis, recordings, and uploads.

warning

authn-service must be tag 2.0.0 or later. Older tags have no push MFA: database logins are refused even when an MFA provider is configured.

Next Steps​

Continue to Network Access: Domain or IP to publish the stack through Nginx.