Home

Deployment Guide

Everything you need to deploy, configure, and run OnlyStatus in production.

Quick Start

Two commands to get running. Requires Docker 24.0+ with Compose v2, at least 2 GB RAM, and x86_64 architecture.

Tinybird analytics does not support ARM natively. Apple Silicon users need Rosetta or QEMU emulation.

quick start
$ git clone https://github.com/neoyubi/onlystatus.git && cd onlystatus
$ ./setup.sh && docker compose up -d

The setup.sh script generates .env.docker with random secrets. First startup takes a few minutes while Docker builds images and initializes databases. Monitor progress:

$ docker compose ps
$ docker compose logs -f
Ready
localhost:3002Dashboard
localhost:3003Status Pages

First Login

Open the dashboard at http://localhost:3002 and click Register. A workspace is created automatically for first-time users.

After creating your account, disable public registration to prevent unauthorized signups:

.env.docker
ALLOW_PUBLIC_REGISTRATION=false

Then restart: docker compose up -d

Production

For production, you need TLS termination via a reverse proxy. OnlyStatus exposes two web-facing services: the dashboard (port 3002) and the status page (port 3003).

Update .env.docker with your production URLs:

.env.docker
NEXT_PUBLIC_URL=https://status.example.com
NEXT_PUBLIC_STATUS_PAGE_BASE_URL=https://pages.example.com
NEXTAUTH_URL=https://status.example.com

Status pages are accessed by path (pages.example.com/myapp) or by custom domain (configured per page in the dashboard). No extra env vars needed. For subdomain routing (myapp.pages.example.com), optionally set STATUS_PAGE_DOMAIN=pages.example.com and configure wildcard DNS + TLS.

Apply the production overlay for resource limits and log rotation:

$ docker compose -f docker-compose.yaml -f docker-compose.prod.yml up -d
WebAuthn / Passkeys

Passkey login requires HTTPS in production. Browsers refuse WebAuthn on plain HTTP (localhost is the only exception, allowed by the spec for development). Set NEXTAUTH_URL to your dashboard's public HTTPS URL and ensure your reverse proxy passes Host and X-Forwarded-Proto headers correctly.

Reverse Proxy

Traefik

If Traefik is already running on your host, create a docker-compose.traefik.yml overlay:

docker-compose.traefik.yml
networks:
traefik_network:
external: true
services:
dashboard:
networks:
- traefik_network
labels:
- "traefik.enable=true"
- "traefik.http.routers.onlystatus-dashboard.rule=Host(`status.example.com`)"
- "traefik.http.routers.onlystatus-dashboard.entrypoints=websecure"
- "traefik.http.routers.onlystatus-dashboard.tls.certresolver=letsencrypt"
- "traefik.http.services.onlystatus-dashboard.loadbalancer.server.port=3000"
status-page:
networks:
- traefik_network
labels:
- "traefik.enable=true"
- "traefik.http.routers.onlystatus-statuspage.rule=Host(`pages.example.com`) || HostRegexp(`^.+\\.pages\\.example\\.com$$`)"
- "traefik.http.routers.onlystatus-statuspage.entrypoints=websecure"
- "traefik.http.routers.onlystatus-statuspage.tls.certresolver=letsencrypt"
- "traefik.http.routers.onlystatus-statuspage.tls.domains[0].main=pages.example.com"
- "traefik.http.routers.onlystatus-statuspage.tls.domains[0].sans=*.pages.example.com"
- "traefik.http.services.onlystatus-statuspage.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik_network"

The HostRegexp matches wildcard subdomains. The tls.domains labels request a wildcard certificate via DNS-01 challenge. If your certresolver only supports HTTP-01, skip the wildcard and use path-based routing instead (pages.example.com/mypage).

$ docker compose -f docker-compose.yaml -f docker-compose.prod.yml -f docker-compose.traefik.yml up -d

nginx

nginx.conf
server {
listen 443 ssl http2;
server_name status.example.com;
ssl_certificate /etc/letsencrypt/live/status.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/status.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl http2;
server_name pages.example.com *.pages.example.com;
ssl_certificate /etc/letsencrypt/live/pages.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pages.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

The X-Forwarded-Host header is required for subdomain routing. For wildcard certs, use certbot with DNS challenge: certbot certonly --dns-<provider> -d pages.example.com -d *.pages.example.com

Caddy

Caddyfile
status.example.com {
reverse_proxy localhost:3002
}
pages.example.com, *.pages.example.com {
reverse_proxy localhost:3003
}

Caddy handles TLS automatically. Wildcard subdomains require a DNS challenge provider. See the Caddy ACME DNS docs.

Status Page Routing

Status pages can be accessed in three ways. Path-based works out of the box. Subdomain routing requires wildcard DNS and TLS.

MethodPatternExample
Path-basedpages.example.com/<slug>pages.example.com/myapp
Subdomain<slug>.pages.example.commyapp.pages.example.com
Custom domainAny domainstatus.yourcompany.com

Custom Domains

Use a fully custom domain like status.yourcompany.com for a specific status page. Four steps are required, as the reverse proxy must route the traffic and the app must know which page to serve.

1
DNS

Create a CNAME or A record pointing your custom domain to the OnlyStatus server.

2
Reverse proxy

Add the domain to your proxy config, routing to port 3003. Pass the Host/X-Forwarded-Host header through.

3
TLS

Ensure your proxy can issue a certificate for the custom domain (HTTP-01 or DNS-01 challenge).

4
Dashboard

Go to the status page settings in the dashboard and enter the custom domain.

The status-page service matches incoming requests against the custom_domain field in the database. If no match is found, it falls back to slug-based lookup.

Private Locations

Deploy lightweight checker containers to any network for distributed monitoring. No inbound ports needed. The checker pulls work and pushes results over HTTPS.

1Create a location in Settings > Private Locations and copy the token
2Deploy the checker container on any Docker-capable machine
3Select the location when creating or editing a monitor
remote checker
$ docker run -d \
$ --name onlystatus-checker \
$ --restart unless-stopped \
$ -e OPENSTATUS_KEY=<your-token> \
$ -e OPENSTATUS_INGEST_URL=https://your-instance.com:8081 \
$ neoyubi/onlystatus-checker:latest

Replace <your-token> with the token from the dashboard and the ingest URL with the public endpoint of your private-location service (port 8081 by default).

Configuration Reference

All settings live in .env.docker. Copy .env.docker.example as your starting point.

Required
AUTH_SECRETSession encryption key
CRON_SECRETAuth token for cron scheduler
Public URLs
NEXT_PUBLIC_URLDashboard's public URL
NEXT_PUBLIC_STATUS_PAGE_BASE_URLStatus page base URL
STATUS_PAGE_DOMAINOptional. Enables subdomain routing (e.g., mypage.pages.example.com). Requires wildcard DNS + TLS.
NEXTAUTH_URLAuth callback URL (set when behind reverse proxy)
Auth
ALLOW_PUBLIC_REGISTRATIONAllow new users to register
TOTP_ENCRYPTION_KEYEncryption key for TOTP 2FA secrets
Email
SMTP_HOSTSMTP server hostname
SMTP_PORTSMTP server port
SMTP_USERSMTP username
SMTP_PASSSMTP password
SMTP_FROMSender email address
Ports
DASHBOARD_PORTDashboard host port
STATUS_PAGE_PORTStatus page host port
SERVER_PORTAPI server host port
CHECKER_PORTChecker host port
PRIVATE_LOCATION_PORTPrivate location ingest port

More in the full guide

Backup and restore, upgrading, troubleshooting, and startup chain details are covered in the repository's deployment guide.

View DEPLOYMENT.md