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.
$ git clone https://github.com/neoyubi/onlystatus.git && cd onlystatus$ ./setup.sh && docker compose up -dThe 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 -flocalhost:3002Dashboardlocalhost:3003Status PagesFirst 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:
ALLOW_PUBLIC_REGISTRATION=falseThen 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:
NEXT_PUBLIC_URL=https://status.example.comNEXT_PUBLIC_STATUS_PAGE_BASE_URL=https://pages.example.comNEXTAUTH_URL=https://status.example.comStatus 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 -dPasskey 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:
networks: traefik_network: external: trueservices: 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 -dnginx
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
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.
| Method | Pattern | Example |
|---|---|---|
| Path-based | pages.example.com/<slug> | pages.example.com/myapp |
| Subdomain | <slug>.pages.example.com | myapp.pages.example.com |
| Custom domain | Any domain | status.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.
Create a CNAME or A record pointing your custom domain to the OnlyStatus server.
Add the domain to your proxy config, routing to port 3003. Pass the Host/X-Forwarded-Host header through.
Ensure your proxy can issue a certificate for the custom domain (HTTP-01 or DNS-01 challenge).
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.
$ 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:latestReplace <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.
| AUTH_SECRET | Session encryption key | required |
| CRON_SECRET | Auth token for cron scheduler | required |
| NEXT_PUBLIC_URL | Dashboard's public URL | http://localhost:3002 |
| NEXT_PUBLIC_STATUS_PAGE_BASE_URL | Status page base URL | http://localhost:3003 |
| STATUS_PAGE_DOMAIN | Optional. Enables subdomain routing (e.g., mypage.pages.example.com). Requires wildcard DNS + TLS. | localhost |
| NEXTAUTH_URL | Auth callback URL (set when behind reverse proxy) |
| ALLOW_PUBLIC_REGISTRATION | Allow new users to register | true |
| TOTP_ENCRYPTION_KEY | Encryption key for TOTP 2FA secrets |
| SMTP_HOST | SMTP server hostname | |
| SMTP_PORT | SMTP server port | 587 |
| SMTP_USER | SMTP username | |
| SMTP_PASS | SMTP password | |
| SMTP_FROM | Sender email address |
| DASHBOARD_PORT | Dashboard host port | 3002 |
| STATUS_PAGE_PORT | Status page host port | 3003 |
| SERVER_PORT | API server host port | 3001 |
| CHECKER_PORT | Checker host port | 8082 |
| PRIVATE_LOCATION_PORT | Private location ingest port | 8081 |
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