openvibers/openvibe.bot Commits

Add production deploy files for Bot (systemd unit, nginx site, README)

OP OpenVibers committed 2 Oct 2026 · 6ff9384f9b · 3 files, +171 −0

Asked: Add the production deploy files Bot is missing (server/index.js already says "see deploy/", but the directory does not exist). Model every file on /home/workstation/OpenVibers/OpenVibe.Codes/deploy/ (read its tree and copy the shape), renaming codes→bot, openvibe.codes→openvibe.bot, /opt/openvibe.c…
Harness job bot-add-the-production-deploy-files-082918 (plan T15).

Changes

ddiff --git a/README.md b/README.md
iindex 75b0553..e34a906 100644
---- a/README.md
++++ b/README.md
@@@ -1,3 +1,14 @@
# OpenVibe.Bot
The open control panel for robots (openvibe.bot). Design: OpenVibe.Contracts docs/adr/ADR-043-bot-devices-and-control.md; plan track T15.
++
++## Deploy
++
++Production runs the Node process under systemd as `openvibe-bot.service` (unit reference: `deploy/systemd/openvibe-bot.service`), with `WorkingDirectory=/opt/openvibe.bot`, `ExecStart` running `server/index.js`, and secrets/config read from the environment file `/etc/openvibe/bot.env`. The app listens on port **4630** behind nginx (`deploy/nginx/openvibe.bot.conf`), which terminates TLS and upgrades the `/device` and `/control` WebSockets to `127.0.0.1:4630`. Readiness is `http://127.0.0.1:4630/api/ready` (liveness: `/api/health`); both are loopback-only.
++
++To run the schema migrations before the service serves, start it once with the direct owner connection in `/etc/openvibe/bot.env` (`DATABASE_DIRECT_URL`); the boot applies `migrations/` and then serves on the pooled `DATABASE_URL`.
++
++## Deploy files
++
++- `deploy/systemd/openvibe-bot.service` — systemd unit (`EnvironmentFile=/etc/openvibe/bot.env`, `PORT=4630`, hardening and `Restart=always`).
++- `deploy/nginx/openvibe.bot.conf` — nginx reference site for openvibe.bot: TLS, www→apex, rate limits, and WebSocket upgrade headers for the device (`/device`) and operator (`/control`) sockets, passing the `Authorization` header through.
ddiff --git a/deploy/nginx/openvibe.bot.conf b/deploy/nginx/openvibe.bot.conf
nnew file mode 100644
iindex 0000000..98a560c
---- /dev/null
++++ b/deploy/nginx/openvibe.bot.conf
@@@ -0,0 +1,129 @@
++## ═══════════════════════════════════════════════════════════════
++## openvibe.bot — Nginx configuration (REFERENCE)
++##
++## Deployed to /etc/nginx/sites-available/openvibe.bot.conf — keep them in sync and reload nginx
++## after changing it. The Node app (port 4630, systemd unit openvibe-bot) serves the REST API and
++## terminates the device (`/device`) and operator (`/control`) WebSockets; nginx terminates TLS,
++## redirects www → apex, upgrades the two WebSocket routes, rate-limits sign-in and the API, and
++## compresses text.
++##
++## No proxy_cache: the app decides cacheability per response (no-store on the API), and device and
++## operator sockets must never be buffered or cached. Never log request bodies or the Authorization
++## header here: the device socket carries its credential in the upgrade's Authorization header.
++## ═══════════════════════════════════════════════════════════════
++
++# WebSocket upgrade: `Connection` is hop-by-hop, so it is derived from the client's request per
++# connection. This map must live in the http context (alongside the limit zones), not in a server.
++map $http_upgrade $connection_upgrade {
++ default upgrade;
++ '' close;
++}
++
++limit_req_zone $binary_remote_addr zone=ovbot_auth:10m rate=10r/m;
++limit_req_zone $binary_remote_addr zone=ovbot_api:10m rate=10r/s;
++
++server {
++ listen 80;
++ listen [::]:80;
++ server_name openvibe.bot www.openvibe.bot;
++
++ location /.well-known/acme-challenge/ { root /var/www/certbot; }
++ location / { return 301 https://openvibe.bot$request_uri; }
++}
++
++server {
++ listen 443 ssl;
++ listen [::]:443 ssl;
++ http2 on;
++ server_name www.openvibe.bot;
++
++ ssl_certificate /etc/letsencrypt/live/openvibe.bot/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/openvibe.bot/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
++
++ return 301 https://openvibe.bot$request_uri;
++}
++
++server {
++ listen 443 ssl;
++ listen [::]:443 ssl;
++ http2 on;
++ server_name openvibe.bot;
++
++ ssl_certificate /etc/letsencrypt/live/openvibe.bot/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/openvibe.bot/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
++
++ access_log /var/log/nginx/openvibe.bot.access.log;
++ error_log /var/log/nginx/openvibe.bot.error.log;
++
++ # The API accepts at most 64 kB of JSON (server/app.js); 1 MB leaves room for headers.
++ client_max_body_size 1m;
++
++ add_header X-Content-Type-Options nosniff always;
++ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
++
++ gzip on;
++ gzip_proxied any;
++ gzip_vary on;
++ gzip_comp_level 5;
++ gzip_min_length 1024;
++ gzip_types text/css application/javascript text/javascript application/json application/problem+json application/schema+json image/svg+xml text/plain application/xml;
++
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ # The device socket authenticates with the credential in this header; forward it verbatim.
++ proxy_set_header Authorization $http_authorization;
++
++ # ── Device WebSocket: one outbound connection per device ──
++ # Authenticated by `Authorization: Bearer <credential>` on the upgrade (never a query string).
++ # Sockets are long-lived and bidirectional, so buffering is off and reads never time out.
++ location = /device {
++ proxy_pass http://127.0.0.1:4630;
++ proxy_set_header Upgrade $http_upgrade;
++ proxy_set_header Connection $connection_upgrade;
++ proxy_buffering off;
++ proxy_read_timeout 3600s;
++ proxy_send_timeout 3600s;
++ }
++
++ # ── Operator/control WebSocket: private, invite and queue access ──
++ location = /control {
++ proxy_pass http://127.0.0.1:4630;
++ proxy_set_header Upgrade $http_upgrade;
++ proxy_set_header Connection $connection_upgrade;
++ proxy_buffering off;
++ proxy_read_timeout 3600s;
++ proxy_send_timeout 3600s;
++ }
++
++ # ── Network SSO (sign-in, callback, sign-out) ───────────
++ location /auth/ {
++ limit_req zone=ovbot_auth burst=10 nodelay;
++ limit_req_status 429;
++ proxy_pass http://127.0.0.1:4630;
++ }
++
++ # ── REST API ────────────────────────────────────────────
++ location /api/ {
++ limit_req zone=ovbot_api burst=20 nodelay;
++ limit_req_status 429;
++ proxy_pass http://127.0.0.1:4630;
++ }
++
++ # ── Prometheus metrics: host-local scrapers only (http://127.0.0.1:4630/metrics) ──
++ location = /metrics { return 404; }
++
++ # ── Everything else (the panel, release.json, static assets) ──
++ location / {
++ proxy_pass http://127.0.0.1:4630;
++ }
++
++ location ~ /\.(?!well-known) {
++ deny all;
++ }
++}
ddiff --git a/deploy/systemd/openvibe-bot.service b/deploy/systemd/openvibe-bot.service
nnew file mode 100644
iindex 0000000..803097a
---- /dev/null
++++ b/deploy/systemd/openvibe-bot.service
@@@ -0,0 +1,31 @@
++[Unit]
++Description=OpenVibe.Bot — the open control panel for robots (robots, devices, pairing, the operator and device WebSockets)
++# Network (sign-in, JWKS, OAuth) should be up first, and Bot's own PostgreSQL (pooled runtime role plus the
++# direct owner role for migrations) and Valkey (state shared between processes) are required to serve; the
++# events relay waits in the outbox. Bot degrades honestly without Events: routes that need it answer 503
++# (see /api/ready).
++After=network.target postgresql.service pgbouncer.service valkey-server.service
++Wants=openvibe-network.service postgresql.service pgbouncer.service valkey-server.service
++
++[Service]
++Type=simple
++User=ubuntu
++Group=ubuntu
++WorkingDirectory=/opt/openvibe.bot
++EnvironmentFile=/etc/openvibe/bot.env
++Environment=NODE_ENV=production
++Environment=PORT=4630
++Environment=PATH=/usr/local/bin:/usr/bin:/bin
++ExecStart=/usr/bin/env node /opt/openvibe.bot/server/index.js
++Restart=always
++RestartSec=5
++KillSignal=SIGTERM
++TimeoutStopSec=10
++NoNewPrivileges=true
++PrivateTmp=true
++ProtectSystem=strict
++ProtectHome=true
++# Bot's own database is PostgreSQL on the host's data role; the code tree stays read-only.
++
++[Install]
++WantedBy=multi-user.target