Upright is a self-hosted synthetic monitoring system. It provides a framework for running health check probes from multiple geographic sites and reporting metrics via Prometheus. Alerts can then be configured with AlertManager.
Features
- Playwright Probes - Browser-based probes for user flows with video recording and logs
- HTTP Probes - Simple HTTP health checks with configurable expected status codes
- SMTP Probes - EHLO handshake verification for mail servers
- Traceroute Probes - Network path analysis with hop-by-hop latency tracking
- Multi-Site Support - Run probes from multiple geographic locations with staggered scheduling
- Observability - OTLP compatible, Prometheus metrics, OpenTelemetry tracing, and AlertManager support
- Configurable Authentication - OmniAuth integration with support for any OIDC provider
Not Included
- Notifications - Instead, Alertmanager is included for alerting and notifications
- Hosting - Instead, you can use a VPS from DigitalOcean, Hetzner, etc.
Components
- Rails engine
- SQLite
- Solid Queue for background and recurring jobs
- Mission Control - Jobs to monitor Solid Queue and manually enqueue probes
- Kamal for deployments
- Prometheus metrics for uptime queries and alerting
- AlertManager for notifications
- Open Telemetry Collector - logs, metrics and traces can be shipped to any OTLP compatible endpoint
Installation
Note
Upright is designed to be run in its own Rails app and deployed with Kamal.
Quick Start (New Project)
Create a new Rails application and install Upright:
rails new my-upright --database=sqlite3 --skip-test
cd my-upright
bundle add upright
bin/rails generate upright:install
bin/rails db:migrateStart the server:
Visit http://app.my-upright.localhost:3000 to see your Upright instance.
Note: Upright uses subdomain-based routing. The
appsubdomain is the admin interface, while site-specific subdomains (e.g.,nyc,lon) show probe results for each location. The.localhostTLD resolves to 127.0.0.1 on most systems.
What the Generator Creates
The upright:install generator creates:
config/initializers/upright.rb- Engine configurationconfig/sites.yml- Site definitions for each VPS you host Upright onconfig/prometheus/prometheus.yml- Prometheus configurationconfig/alertmanager/alertmanager.yml- AlertManager configurationconfig/otel_collector.yml- OpenTelemetry Collector configurationprobes/- Directory for all HTTP, SMTP, Traceroute YAML config as well as Playwright probe classes
It also mounts the engine at / in your routes.
Configuration
Basic Setup
See config/initializers/upright.rb
Hostname Configuration
Upright uses subdomain-based routing. Configure your production hostname:
# config/initializers/upright.rb Upright.configure do |config| config.hostname = "upright.com" end
For local development, the hostname defaults to {service_name}.localhost (e.g., upright.localhost).
Site Configuration
Define your monitoring locations in config/sites.yml:
shared: sites: - code: nyc city: New York City country: US geohash: dr5reg provider: digitalocean - code: ams city: Amsterdam country: NL geohash: u17982 provider: digitalocean - code: sfo city: San Francisco country: US geohash: 9q8yy provider: hetzner
Each site node identifies itself via the SITE_SUBDOMAIN environment variable, configured in your Kamal deploy.yml.
Authentication
Static Credentials
Upright uses static credentials by default with username admin and password upright.
Warning
Change the default password before deploying to production by setting the ADMIN_PASSWORD environment variable.
OpenID Connect
For production environments, Upright supports OpenID Connect (Logto, Keycloak, Duo, Okta, etc.):
# config/initializers/upright.rb Upright.configure do |config| config.auth_provider = :openid_connect config.auth_options = { issuer: "https://your-tenant.logto.app/oidc", client_id: ENV["OIDC_CLIENT_ID"], client_secret: ENV["OIDC_CLIENT_SECRET"] } end
Probe Result Cleanup
Upright automatically cleans up old probe results on a recurring schedule. You can configure the retention thresholds:
Upright.configure do |config| config.stale_success_threshold = 24.hours # Delete successful results older than this (default: 24 hours) config.stale_failure_threshold = 30.days # Delete failed results older than this (default: 30 days) config.failure_retention_limit = 20_000 # Keep at most this many failed results (default: 20,000) end
Defining Probes
HTTP Probes
Add probes to probes/http_probes.yml:
- name: Main Website url: https://example.com expected_status: 200 - name: API Health url: https://api.example.com/health expected_status: 200 alert_severity: critical - name: Admin Panel url: https://admin.example.com basic_auth_credentials: admin_auth # Key in Rails credentials
The optional alert_severity field controls the Prometheus alert severity when a probe fails. Values: medium, high (default), critical.
SMTP Probes
Add probes to probes/smtp_probes.yml:
- name: Primary Mail Server host: mail.example.com - name: Backup Mail Server host: mail2.example.com
Playwright Probes
Generate a new browser-based probe:
bin/rails generate upright:playwright_probe MyServiceAuth
This creates a probe class:
# probes/my_service_auth_probe.rb class Probes::Playwright::MyServiceAuthProbe < Upright::Probes::Playwright::Base # Optionally authenticate before running # authenticate_with_form :my_service def check page.goto("https://app.example.com") page.fill('[name="email"]', "test@example.com") page.click('button[type="submit"]') page.wait_for_selector(".dashboard") end end
See https://playwright-ruby-client.vercel.app/docs/api/page for how to create Playwright tests.
Creating Authenticators
For probes that require authentication, create an authenticator:
# probes/authenticators/my_service.rb class Playwright::Authenticator::MyService < Upright::Playwright::Authenticator::Base def signin_redirect_url = "https://app.example.com/dashboard" def signin_path = "/login" def service_name = :my_service def authenticate page.goto("https://app.example.com/login") page.get_by_label("Email").fill(credentials.my_service.email) page.get_by_label("Password").fill(credentials.my_service.password) page.get_by_role("button", name: "Sign in").click end end
Scheduling
Configure probe scheduling with Solid Queue in config/recurring.yml:
production: http_probes: command: "Upright::Probes::HTTPProbe.check_and_record_all_later" schedule: every 30 seconds smtp_probes: command: "Upright::Probes::SMTPProbe.check_and_record_all_later" schedule: every 30 seconds my_service_auth: command: "Probes::Playwright::MyServiceAuthProbe.check_and_record_later" schedule: every 15 minutes
System Requirements
Minimum VM Specifications
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 vCPU | 2 vCPU |
| RAM | 2 GB | 4 GB |
| Disk | 25 GB | 50 GB |
Playwright browser automation is memory-intensive. For sites running many Playwright probes concurrently, consider 4 GB RAM.
Software Requirements
- OS: Ubuntu 24.04+ or Debian 12+ (any Linux with Docker support)
- Docker: 24.0+ (installed automatically by Kamal)
- Ruby: 3.4+ (for local development only; production runs in Docker)
- Rails: 8.0+
Firewall Rules
Open the following ports:
| Port | Protocol | Direction | Purpose |
|---|---|---|---|
| 22 | TCP | Inbound | SSH access |
| 80 | TCP | Inbound | HTTP (redirects to HTTPS) |
| 443 | TCP | Inbound | HTTPS |
| 25 | TCP | Outbound | SMTP probes (if used) |
SMTP Port 25 Note
Most cloud providers block outbound port 25 by default to prevent spam. If you plan to use SMTP probes, you must request port 25 to be unblocked.
This is not required for HTTP or Playwright probes.
DNS Setup
For multiple geographic locations, use subdomains for each site. Each subdomain points to a different server:
; Primary dashboard
app.upright.example.com A 203.0.113.10
; Monitoring nodes
ams.upright.example.com A 203.0.113.10 ; Amsterdam
nyc.upright.example.com A 198.51.100.20 ; New York
sfo.upright.example.com A 192.0.2.30 ; San Francisco
Deployment with Kamal
Example config/deploy.yml
service: upright image: your-org/upright servers: web: hosts: - ams.upright.example.com: [amsterdam] - nyc.upright.example.com: [new_york] - sfo.upright.example.com: [san_francisco] jobs: hosts: - ams.upright.example.com: [amsterdam] - nyc.upright.example.com: [new_york] - sfo.upright.example.com: [san_francisco] cmd: bin/jobs proxy: app_port: 3000 ssl: true hosts: - "*.upright.example.com" env: secret: - RAILS_MASTER_KEY tags: amsterdam: SITE_SUBDOMAIN: ams new_york: SITE_SUBDOMAIN: nyc san_francisco: SITE_SUBDOMAIN: sfo accessories: playwright: image: jacoblincool/playwright:chromium-server-1.55.0 port: "127.0.0.1:53333:53333" roles: - jobs prometheus: image: prom/prometheus:v3.2.1 hosts: - ams.upright.example.com cmd: >- --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d --web.enable-otlp-receiver files: - config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - config/prometheus/rules/upright.rules.yml:/etc/prometheus/rules/upright.rules.yml alertmanager: image: prom/alertmanager:v0.28.1 hosts: - ams.upright.example.com cmd: --config.file=/etc/alertmanager/alertmanager.yml files: - config/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
Observability
Prometheus
Metrics are exposed via a Puma plugin at http://0.0.0.0:9394/metrics. Configure Prometheus to scrape:
scrape_configs: - job_name: upright static_configs: - targets: ['localhost:9394']
Metrics Exposed
upright_probe_duration_seconds- Probe execution durationupright_probe_up- Probe status (1 = up, 0 = down)upright_http_response_status- HTTP response status code
Labels include: type, name, site_code, site_city, site_country
AlertManager
Example alert rules (prometheus/rules/upright.rules):
groups: - name: upright rules: - alert: ProbeDown expr: upright_probe_up == 0 for: 5m labels: severity: "{{ $labels.alert_severity }}" annotations: summary: "Probe {{ $labels.name }} is down"
OpenTelemetry
Traces are automatically created for each probe execution. Configure your collector endpoint:
Upright.configure do |config| config.otel_endpoint = "https://otel.example.com:4318" end
Local Development
Setup
This installs dependencies, prepares the database, and starts the dev server.
Running Services
Start supporting Docker services (Playwright server, etc.):
Running the Server
Visit http://app.upright.localhost:3000 and sign in with:
- Username:
admin - Password:
upright(or value ofADMIN_PASSWORDenv var)
Testing Playwright Probes
Run probes with a visible browser window:
LOCAL_PLAYWRIGHT=1 bin/rails console
Probes::Playwright::MyServiceAuthProbe.check
Running Tests
License
The gem is available under the terms of the MIT License.