From 05de838b0f82275ff897d6c0b637c77b43a72ae0 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Sat, 25 Jul 2026 19:15:03 +0000 Subject: [PATCH] better self host doc (#2440) --- .env.docker.example | 13 +- .../guides/how-to-create-private-location.mdx | 9 +- .../docs/guides/self-hosting-openstatus.mdx | 232 +++++++++++++----- 3 files changed, 184 insertions(+), 70 deletions(-) diff --git a/.env.docker.example b/.env.docker.example index b7bb3268..d146a300 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -31,8 +31,15 @@ QSTASH_URL=https://qstash.upstash.io/v1/publish/ # Tinybird (optional - for monitor analytics and charts) # Leave empty to disable analytics features # Get from: https://www.tinybird.co +# Self-hosted with the local container: tb --local info (use the `token` value; +# NOT curl :7181/tokens, which returns a different workspace) TINY_BIRD_API_KEY= +# Same token as TINY_BIRD_API_KEY, read by the private-location ingest server (Go). +# Both must be set: the ingest server never reads TINY_BIRD_API_KEY, so leaving +# this empty makes Tinybird reject every check with a 403. +TINYBIRD_TOKEN= + # Tinybird URL (optional - defaults to cloud API) # For local Tinybird container, set to: http://tinybird-local:7181 TINYBIRD_URL= @@ -138,8 +145,10 @@ NEXT_PUBLIC_URL=http://localhost:3002 # Screenshot service (optional) SCREENSHOT_SERVICE_URL= -# External services -OPENSTATUS_INGEST_URL=https://openstatus-private-location.fly.dev +# NOTE: OPENSTATUS_INGEST_URL does not belong here. No service in this compose +# stack reads it -- it configures the *probe* (ghcr.io/openstatushq/private-location), +# which runs on whatever host you monitor from. Set it there, alongside +# OPENSTATUS_KEY, pointing at this stack's private-location service on port 8081. # Docs knowledge base for the AI assistant / MCP server (optional) # The search_docs and get_doc_page tools query this site's /api/search and diff --git a/apps/web/src/content/pages/docs/guides/how-to-create-private-location.mdx b/apps/web/src/content/pages/docs/guides/how-to-create-private-location.mdx index 0e31f569..bf085df2 100644 --- a/apps/web/src/content/pages/docs/guides/how-to-create-private-location.mdx +++ b/apps/web/src/content/pages/docs/guides/how-to-create-private-location.mdx @@ -36,7 +36,7 @@ Run the monitoring probe on your server using Docker: docker run -d \ --name openstatus-probe \ --restart unless-stopped \ - -e OPENSTATUS_TOKEN= \ + -e OPENSTATUS_KEY= \ ghcr.io/openstatushq/private-location:latest ``` @@ -87,13 +87,14 @@ docker logs openstatus-probe Common causes: - **Invalid token** — double-check the token value; ensure no extra spaces or newlines. -- **Network issues** — ensure the container can reach `api.openstatus.dev` on port 443. +- **Wrong variable name** — the probe reads `OPENSTATUS_KEY`. `OPENSTATUS_TOKEN` is ignored, so the probe starts unauthenticated and fetches no monitors. +- **Network issues** — ensure the container can reach `openstatus-private-location.fly.dev` on port 443. ### No data appearing in the dashboard 1. Verify the container is running: `docker ps --filter name=openstatus-probe` 2. Check that you've assigned at least one monitor to the private location -3. Wait 2-3 minutes — there may be a short delay before the first check runs +3. Wait for the probe to pick up the change. It fetches its monitor list on startup and then every 10 minutes, so a monitor assigned after the probe started can take that long to appear. Restart the container to apply it immediately. ### Container can't reach internal services @@ -104,7 +105,7 @@ docker run -d \ --name openstatus-probe \ --restart unless-stopped \ --network host \ - -e OPENSTATUS_TOKEN= \ + -e OPENSTATUS_KEY= \ ghcr.io/openstatushq/private-location:latest ``` diff --git a/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx b/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx index 79ed3bb4..a1395067 100644 --- a/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx +++ b/apps/web/src/content/pages/docs/guides/self-hosting-openstatus.mdx @@ -20,6 +20,22 @@ openstatus provides a Docker Compose setup that makes self-hosting straightforwa - Basic understanding of Docker and containerization - Command line experience - Git installed +- The [Tinybird CLI](https://www.tinybird.co/docs/cli) (`tb`) — required for the analytics setup in Part 2 + +## Two different things are called "private location" + +This trips up almost everyone, so read this before you start. There are **two** separate components, with two confusingly similar image names: + +| | Image | What it is | Where it runs | +|---|---|---|---| +| **Probe** | `ghcr.io/openstatushq/private-location:latest` | Runs the actual checks against your endpoints and ships results to the ingest server | Anywhere you want to monitor *from* — a VPS, a Raspberry Pi, inside your VPC. **Not** part of `docker-compose.yaml`. | +| **Ingest server** | `ghcr.io/openstatushq/openstatus-private-location:latest` | Receives results from probes, writes them to Tinybird, and forwards status changes to the workflows app | On your openstatus host, as the `private-location` service in `docker-compose.yaml` (published on port **8081**) | + +The probe's `OPENSTATUS_INGEST_URL` points at the **ingest server** — that is port **8081**, not 3001. Port 3001 is the API server (`apps/server`) and has nothing to do with ingest. + + ## Known limitations @@ -31,7 +47,7 @@ Self-hosting openstatus currently has these constraints: ## Step-by-step guide -This guide is divided into three parts: launching the services, setting up the database and analytics, and configuring the application through the UI. +This guide is divided into three parts: launching the services, setting up analytics, and configuring the application through the UI. ### Part 1: initial setup and service launch @@ -51,6 +67,20 @@ This guide is divided into three parts: launching the services, setting up the d ``` Open `.env.docker` in a text editor. At a minimum, you **must** set a value for `AUTH_SECRET` for authentication to work. For a complete setup, review the file for other variables like OAuth providers or email services. + Two values matter for the rest of this guide — set them now: + ```env + # Point the apps at the local Tinybird container instead of Tinybird Cloud. + # If you leave this empty, the dashboard queries api.tinybird.co and every + # chart fails with "the pipe ... does not exist". + TINYBIRD_URL=http://tinybird-local:7181 + + # Shared secret the ingest server uses to authenticate to the workflows app. + # Any random string, but it must be set on both. + CRON_SECRET=some-random-string + ``` + + You'll fill in the Tinybird tokens in Part 2, once the container is running. + 3. **Build and start services** Use Docker Compose to build and run all openstatus services in the background. @@ -60,70 +90,96 @@ This guide is divided into three parts: launching the services, setting up the d ``` You can check the status of the services with `docker compose ps`. It might take a few minutes for all services to be healthy. -### Part 2: database and analytics setup + Database migrations run automatically: the `db-migrate` one-shot container applies them before the apps start, and every other service waits on it. It's idempotent, so it's safe to leave in place on each `docker compose up`. If it fails, see [Troubleshooting](#troubleshooting). -4. **Run database migrations** + - The database container starts with an empty database. You must run migrations to set up the required schema. - ```bash - # Make sure you are in the root of the openstatus project - cd packages/db - pnpm install - pnpm migrate - cd ../.. # Return to the project root - ``` +### Part 2: analytics setup + +openstatus stores every check result in Tinybird. Without a working Tinybird setup you can create monitors and probes, but the dashboard will show no data. + +This is the step most self-hosters get wrong, so follow it exactly. + +4. **Deploy the Tinybird project** - If you do not have or want to avoid installing the necessary tools on the host, you can run this command to create a one-shot container that will remove itself after completion. - ``` - # Make sure you are in the root of the openstatus project - # For RHEL derivatives, make sure to end /work with :Z for SELinux, "$PWD":/work:Z - sudo docker run --rm -it \ - --network openstatus \ - --env-file .env.docker \ - -v "$PWD":/work \ - -w /work/packages/db \ - node:22-trixie \ - bash -lc ' - set -euo pipefail - export DEBIAN_FRONTEND=noninteractive - apt-get update -qq - apt-get install -y -qq curl ca-certificates unzip - curl -fsSL https://bun.sh/install -o /tmp/bun-install.sh - bash /tmp/bun-install.sh - export PATH="$HOME/.bun/bin:$PATH" - npm i -g pnpm - pnpm install - bun src/migrate.mts - ' - ``` - -5. **Deploy local Tinybird analytics** - - Tinybird is used for analytics. Deploy the local datasources, pipes, and endpoints. + Deploy the datasources, pipes, and endpoints into the local Tinybird container started by Compose: ```bash - # Make sure you are in the root of the openstatus project cd packages/tinybird - pnpm install tb --local deploy - cd ../.. # Return to the project root ``` -6. **Configure Tinybird API key** +5. **Confirm the deployment is live** - You need to get your local Tinybird admin token and add it to your environment file. + A deployment that stays in `Staging` is not queryable — the pipes exist but every request 404s. Check it: ```bash - cd packages/tinybird - tb --local open # This opens the Tinybird UI in your browser + tb --local deployment ls + ``` + You want `Status` to read `Live`: + ``` + ------------------------------------- + | ID | Status | Created at | + ------------------------------------- + | 1 | Live | 2026-07-25 18:20:52 | + ------------------------------------- + ``` + If it says `Staging`, promote it: + ```bash + tb --local deployment promote + ``` + +6. **Get the Tinybird token** + + You do **not** need to log in to Tinybird Cloud. Against a local container the CLI runs in a temporary local workspace and prints its token: + ```bash + tb --local info ``` - In the Tinybird UI, find and copy your admin token. Then, add it to your `.env.docker` file in the root of the project: + Copy the value of `token` from the **Tinybird Local** section. + + To grab it in one line: + ```bash + tb --local info | grep '^token:' | awk '{print $2}' + ``` + + + + Likewise, `tb --local open` asks you to sign in to Tinybird Cloud and is not usable on a headless server. You don't need it. + +7. **Add the token to your environment** + + The Node apps and the Go ingest server read **different variable names** for the same token. Both are in `.env.docker` — fill them in with the same value: ```env - TINY_BIRD_API_KEY="your-tinybird-admin-token" + # Read by the dashboard, status page, server, and workflows apps + TINY_BIRD_API_KEY="your-tinybird-local-token" + + # Read by the private-location ingest server (Go) + TINYBIRD_TOKEN="your-tinybird-local-token" + ``` + + + + Restart so the new values are picked up: + ```bash + cd ../.. + docker compose up -d ``` - After adding the token, restart your services for the changes to take effect: + +8. **Verify analytics end to end** + + Confirm the token can query a deployed pipe: ```bash - # Make sure you are in the root of the openstatus project - docker compose restart + TOKEN=$(tb --local info | grep '^token:' | awk '{print $2}') + + curl -s -o /dev/null -w '%{http_code}\n' \ + -H "Authorization: Bearer $TOKEN" \ + "http://localhost:7181/v0/pipes/endpoint__http_metrics_1d__v1.json?monitorId=1" ``` + A `200` means Part 2 is done. A `404` means the deployment isn't promoted or you're using the wrong token — go back to steps 5 and 6. ### Part 3: application configuration @@ -132,7 +188,7 @@ Now that the services are running, you can access the dashboard and perform the - **Dashboard:** `http://localhost:3002` - **Status Pages:** `http://localhost:3003` -7. **Create a workspace and set limits** +9. **Create a workspace and set limits** - Navigate to the dashboard at `http://localhost:3002`. - Sign up and create a new workspace. @@ -155,18 +211,42 @@ Now that the services are running, you can access the dashboard and perform the ]}" ``` -8. **Deploy a private location** +10. **Deploy a probe** + + The self-hosted version relies on private locations to perform checks. The ingest server is already running from Part 1 — what you deploy here is the **probe**. + + - In the dashboard, navigate to **Settings → Private Locations** and create a new one. + - Copy the generated key. + - Run the probe wherever you want to check from: + + ```bash + docker run -d --name openstatus-probe \ + -e OPENSTATUS_KEY= \ + -e OPENSTATUS_INGEST_URL=http://:8081 \ + ghcr.io/openstatushq/private-location:latest + ``` + + The two environment variables are: + + - `OPENSTATUS_KEY` — the key you copied from the dashboard. Some older docs call this `OPENSTATUS_TOKEN`; that name is not read by the probe. + - `OPENSTATUS_INGEST_URL` — the URL of your **ingest server**, i.e. the `private-location` service on port **8081**. If you don't set it, the probe defaults to openstatus Cloud (`https://openstatus-private-location.fly.dev`) and your self-hosted instance will never see a check. + + If you run the probe on the same Docker network as the rest of the stack, use the internal address instead — `http://private-location:8080`. + + A healthy probe logs like this: + ``` + Launching openstatus private location checker + 2026/07/25 09:50:50 Starting job for monitor 1 (https://google.com) + 2026/07/25 09:50:50 Monitor check for 1 (https://google.com) ingested with status "success" (code 200) + ``` + + - The self-hosted version relies on private locations to perform checks. - - In the dashboard, navigate to **Settings -> Private Locations** and create a new one. - - Copy the generated `OPENSTATUS_KEY`. - - Deploy the private location probe to your infrastructure using the Docker image `ghcr.io/openstatushq/private-location:latest`. - - When deploying, you must provide two environment variables to the container: - - `OPENSTATUS_KEY`: The key you just copied. - - `OPENSTATUS_INGEST_URL`: The URL of your self-hosted server's API endpoint (e.g., `http://:3001`). - For a detailed guide on deploying a private location, see **[Deploy Private Locations on Cloudflare Containers](/docs/guides/how-to-deploy-probes-cloudflare-containers)**. -9. **Create monitors** +11. **Create monitors** You're all set! You can now create monitors in the dashboard. They will be checked by the private location you deployed. @@ -218,15 +298,18 @@ openstatus consists of multiple services running together: | Service | Port | Purpose | |---------|------|---------| +| db-migrate | — | One-shot database migrations; exits after applying | | workflows | 3000 | Background jobs and scheduled tasks | | server | 3001 | API backend (tRPC) | | dashboard | 3002 | Admin interface for configuration | | status-page | 3003 | Public status pages | -| private-location | 8081 | Monitoring agent for checks | +| private-location | 8081 | **Ingest server** — receives results from probes | | libsql | 8080 | Database (HTTP) | | libsql | 5001 | Database (gRPC) | | tinybird-local | 7181 | Analytics and metrics | +The **probe** is not in this table — it runs outside the stack, wherever you want to check from, and talks to `private-location` on port 8081. + ## What you've accomplished - Deployed openstatus on your own infrastructure @@ -238,11 +321,32 @@ openstatus consists of multiple services running together: **Containers won't start** — check Docker logs with `docker compose logs [service-name]`. -**Database migrations fail** — ensure you're in the correct directory and have pnpm installed. +**Database migrations fail** — inspect `docker compose logs db-migrate`. To re-run them by hand without installing anything on the host (the image's entrypoint is the migration itself): +```bash +docker run --rm -it \ + --network openstatus \ + --env-file .env.docker \ + -e DATABASE_URL=http://libsql:8080 \ + openstatus/db-migrate:latest +``` +Or simply `docker compose up -d db-migrate`, since the step is idempotent. + +**Dashboard charts are empty and the log shows `The pipe 'endpoint__http_metrics_1d__v1' does not exist`** — the dashboard is querying the wrong Tinybird workspace. Three causes, in order of likelihood: +1. `TINYBIRD_URL` is unset, so it's querying Tinybird Cloud. Set `TINYBIRD_URL=http://tinybird-local:7181`. +2. The token came from `curl http://localhost:7181/tokens` instead of `tb --local info` — wrong workspace. See step 6. +3. The deployment is still in `Staging`. Run `tb --local deployment promote`. + +**Ingest server logs `tinybird.success=false error.message="unexpected status code: 403"`** — `TINYBIRD_TOKEN` is missing or wrong. It is a *separate* variable from `TINY_BIRD_API_KEY`; both must be set to the same token. See step 7. + +**Ingest server logs `failed to forward status update to workflows ... 401`** — the `CRON_SECRET` the ingest server sends doesn't match the one the workflows app expects. Make sure `CRON_SECRET` is set in `.env.docker` and that both containers were restarted afterwards. If the ingest server runs outside the Compose network, also confirm `WORKFLOWS_URL` points at your own workflows app — unset, it defaults to openstatus Cloud, which will reject your secret. -**Private location not connecting** — verify the `OPENSTATUS_KEY` and `OPENSTATUS_INGEST_URL` are correct. +**Private location shows an error state in the dashboard but the probe logs look fine** — a cron in the workflows app marks a location unhealthy when it hasn't reported recently. Confirm `workflows` is running and healthy (`docker compose ps workflows`), and that the ingest server can reach it. -**Tinybird issues** — make sure the Tinybird token is correctly set in `.env.docker`. +**Probe runs but nothing appears in the dashboard** — check, in order: +1. `OPENSTATUS_INGEST_URL` points at port **8081** (the ingest server), not 3001. +2. `OPENSTATUS_KEY` matches the key shown in **Settings → Private Locations**. +3. The ingest server logs show `IngestHTTP` requests arriving — `docker compose logs private-location`. +4. Those log lines show `tinybird.success=true`. If not, revisit step 7. ## What's next -- 2.51.2