Deploying sentrinel.dev

Three static sites on Cloudflare, one application stack on your own servers. Nothing here needs a secret in the repository.

The shape of it

Host Serves Where it runs
sentrinel.dev marketing site Cloudflare Pages
www.sentrinel.dev redirect to the apex Cloudflare rule
docs.sentrinel.dev documentation Cloudflare Pages
app.sentrinel.dev the dashboard Cloudflare Pages
api.sentrinel.dev the Sentrinel API application server, via Dokploy
status.sentrinel.dev public status pages the same API

The dashboard is on the CDN rather than the server because every one of its 24 pages prerenders โ€” it is a client-side app that calls the API cross-origin, with no route handlers and no middleware. That means no Node process to keep alive, no cold starts, and the origin only ever serves API traffic.

The API cannot go on Cloudflare: it holds long-lived connections to Postgres and ClickHouse, runs the alert checker and uptime prober on timers, and streams SSE. It belongs on the box with the databases.

Every record is proxied (orange cloud), so the origin IP is never published and TLS terminates at the edge.


1. Credentials

cp .env.deploy.example .env.deploy

Fill it in. The file is gitignored and is the only place these values live.

Use a scoped API Token, not the Global API Key. A Global key authenticates as your entire account with no way to narrow it โ€” anything holding it can change billing, delete zones and read every other domain you own, and it cannot be revoked without breaking everything else using it. A token can be limited to exactly this:

Create one at dash.cloudflare.com/profile/api-tokens.


2. All of it, one command

bun run deploy

Preflight โ†’ DNS โ†’ build and publish the three sites โ†’ preflight again. It stops at the first real problem rather than half-deploying, and every step below can also be run on its own.

To look without touching anything:

bun run deploy:check

That reads the token and its scopes, every DNS record, whether the Pages projects exist and have their custom domains, whether all five hostnames answer, and whether Dokploy is reachable โ€” and says what it found for each. It is the right thing to paste when you want someone to diagnose a deploy.


3. DNS

bun run deploy:dns            # prints the plan, changes nothing
bun run deploy:dns -- --apply

It reconciles rather than appends, so running it twice is a no-op, and it lists but never touches records it does not manage โ€” your mail and verification records are safe.


4. The three sites

bun run deploy:pages              # all three
bun run deploy:pages dashboard    # or one at a time

First publish only: attach the custom domain to each project under Workers & Pages โ†’ <project> โ†’ Custom domains.

NEXT_PUBLIC_API_URL is compiled into the dashboard bundle, so pointing it at a different API is a rebuild, not a restart.

www โ†’ apex

Cloudflare dashboard โ†’ Rules โ†’ Redirect Rules โ†’ Create:


5. The API on Dokploy

Your Dokploy already runs other projects; this adds one more beside them.

Create the project

Dokploy โ†’ Projects โ†’ Create โ†’ sentrinel โ†’ Add Service โ†’ Compose.

Point it at this repository, compose file docker-compose.prod.yml.

Environment

Set these in Dokploy's environment editor โ€” not in a committed file:

POSTGRES_PASSWORD=<generate>
CLICKHOUSE_PASSWORD=<generate>
SENTRINEL_REQUIRE_AUTH=true
SENTRINEL_REQUIRE_INGEST_KEY=true
SENTRINEL_ALLOWED_ORIGINS=https://app.sentrinel.dev
PUBLIC_API_URL=https://api.sentrinel.dev
PUBLIC_DASHBOARD_URL=https://app.sentrinel.dev
TELEMETRY_STORE=clickhouse

SENTRINEL_ALLOWED_ORIGINS is the security boundary, not a convenience: the dashboard sends its session cookie with every request, so an origin that is not on this list must not be able to call the API. Unset means localhost only.

Domain

Dokploy โ†’ the api service โ†’ Domains โ†’ Add:

Add status.sentrinel.dev the same way if you want the public status pages on their own host.

Database server

If Postgres and ClickHouse live on the second box rather than in the compose file, remove those two services and point the API at them:

DATABASE_URL=postgres://sentrinel:<password>@75.119.137.38:5432/sentinel
CLICKHOUSE_URL=http://75.119.137.38:8123

Then restrict both to the application server's address โ€” a database reachable from the whole internet is the single most common way these deployments are lost:

ufw allow from 194.163.176.236 to any port 5432 proto tcp
ufw allow from 194.163.176.236 to any port 8123 proto tcp
ufw deny 5432
ufw deny 8123

First run

docker compose -f docker-compose.prod.yml exec api \
  bun run --cwd packages/db ch:migrate

Then open https://app.sentrinel.dev and create the first account โ€” it becomes the owner of a new organization.


6. Check it

curl -s https://api.sentrinel.dev/health
{ "status": "ok", "telemetryStore": "clickhouse", "telemetryHealthy": true }

The API prints its security posture at boot. Read it after every deploy:

๐Ÿ›ก๏ธ  Sentrinel API on :3001
    auth        required
    ingest key  required
    telemetry   clickhouse
    CORS        https://app.sentrinel.dev

auth OFF or a CORS line reading localhost only means the environment did not reach the container.


Rotating a credential

Any secret that has been pasted into a chat, a ticket, or a screenshot is spent โ€” treat it as public and replace it: