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.
![]() |
![]() |
| Site overview with world map | 30-day uptime history |
![]() |
|
| Probe status across all sites | |
- 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
- Notifications - Instead, Alertmanager is included for alerting and notifications
- Hosting - Instead, you can use a VPS from DigitalOcean, Hetzner, etc.
- 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
Note
Upright is designed to be run in its own Rails app and deployed with Kamal.
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:
bin/devVisit 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.
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.
See config/initializers/upright.rb
Upright uses subdomain-based routing. Configure your production hostname:
# config/initializers/upright.rb
Upright.configure do |config|
config.hostname = "upright.com"
endFor local development, the hostname defaults to {service_name}.localhost (e.g., upright.localhost).
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: hetznerEach site node identifies itself via the SITE_SUBDOMAIN environment variable, configured in your Kamal deploy.yml.
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.
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"]
}
endAdd 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
- name: Admin Panel
url: https://admin.example.com
basic_auth_credentials: admin_auth # Key in Rails credentialsAdd probes to probes/smtp_probes.yml:
- name: Primary Mail Server
host: mail.example.com
- name: Backup Mail Server
host: mail2.example.comGenerate a new browser-based probe:
bin/rails generate upright:playwright_probe MyServiceAuthThis 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
endSee https://playwright-ruby-client.vercel.app/docs/api/page for how to create Playwright tests.
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
endConfigure 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| 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.
- 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+
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) |
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.
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
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.ymlMetrics 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']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
Example alert rules (prometheus/rules/upright.rules):
groups:
- name: upright
rules:
- alert: ProbeDown
expr: upright_probe_up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Probe {{ $labels.name }} is down"Traces are automatically created for each probe execution. Configure your collector endpoint:
Upright.configure do |config|
config.otel_endpoint = "https://otel.example.com:4318"
endbin/setupThis installs dependencies, prepares the database, and starts the dev server.
Start supporting Docker services (Playwright server, etc.):
bin/servicesbin/devVisit http://app.upright.localhost:3000 and sign in with:
- Username:
admin - Password:
upright(or value ofADMIN_PASSWORDenv var)
Run probes with a visible browser window:
LOCAL_PLAYWRIGHT=1 bin/rails consoleProbes::Playwright::MyServiceAuthProbe.checkbin/rails testThe gem is available under the terms of the MIT License.


