From 02412169a0097a4aafb0d0970c56628efab696df Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Mon, 11 May 2026 10:08:33 +0200 Subject: [PATCH] fix(deploy): make Docker the service runtime Remove host daemon deployment for server, proxy, and Node-RED so Node-RED is only reachable through the Compose proxy boundary. --- deploy/README.md | 117 ++++++++---------- deploy/angie/betterframe.conf | 133 --------------------- deploy/angie/betterframe.docker.conf | 4 +- deploy/docker/docker-compose.yml | 12 +- deploy/nodered/settings.js | 11 -- deploy/systemd/betterframe-nodered.service | 27 ----- deploy/systemd/betterframe-server.service | 29 ----- 7 files changed, 56 insertions(+), 277 deletions(-) delete mode 100644 deploy/angie/betterframe.conf delete mode 100644 deploy/nodered/settings.js delete mode 100644 deploy/systemd/betterframe-nodered.service delete mode 100644 deploy/systemd/betterframe-server.service diff --git a/deploy/README.md b/deploy/README.md index b6295a3..0094c54 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,101 +1,80 @@ # BetterFrame deployment -## Native install (Raspberry Pi) +## Recommended: Docker services + native kiosk -### Server +Run server, Angie/nginx, and Node-RED in Docker Compose. Only Angie publishes a +host port. The BetterFrame backend ports and Node-RED are internal to the Docker +network, which forces `/nrdp/`, `/in/kiosk/`, and admin traffic through the +proxy auth rules. ```bash -# Install Node.js 23 + Node-RED -curl -fsSL https://deb.nodesource.com/setup_23.x | sudo bash - -sudo apt install -y nodejs build-essential -sudo npm install -g --unsafe-perm node-red - -# Create user + dirs -sudo useradd -r -m -d /var/lib/betterframe betterframe -sudo mkdir -p /opt/betterframe /var/log/betterframe /etc/betterframe /var/lib/betterframe/nodered -sudo chown -R betterframe:betterframe /var/lib/betterframe /var/log/betterframe - -# Deploy code -sudo git clone https://github.com/BetterCorp/BetterFrame.git /opt/betterframe cd /opt/betterframe -sudo -u betterframe npm install -sudo -u betterframe npm run build -sudo cp sec-config.yaml /opt/betterframe/server/sec-config.yaml - -# Install systemd units -sudo cp deploy/systemd/betterframe-server.service /etc/systemd/system/ -sudo cp deploy/systemd/betterframe-nodered.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now betterframe-server betterframe-nodered +docker compose -f deploy/docker/docker-compose.yml up -d --build ``` -The native config binds BetterFrame service ports and Node-RED to `127.0.0.1`. -Do not expose ports `18080`, `18081`, `18082`, or `1880` directly on the LAN. -Use Angie/nginx as the public entry point so `/nrdp/`, `/in/kiosk/`, and the -admin routes get the auth protections in `deploy/angie/betterframe.conf`. +Published: -### Kiosk +- `80` -> Angie/nginx public edge + +Internal only: + +- `18080` -> admin service +- `18081` -> kiosk API service +- `18082` -> kiosk WebSocket service +- `1880` -> Node-RED + +Access first-run setup at: + +```text +http:///setup +``` + +Node-RED is reachable only through: + +```text +http:///nrdp/ +``` + +Do not publish `18080`, `18081`, `18082`, or `1880` on the host. + +If migrating from an older native install, stop the old host daemons first: + +```bash +sudo systemctl disable --now betterframe-server betterframe-nodered angie nginx 2>/dev/null || true +``` + +## Kiosk + +The kiosk still runs natively on the Pi because it needs Wayland/HDMI, GTK, +GStreamer, display power control, and local hardware access. ```bash -# Install GTK4 + GStreamer + WebKit sudo apt install -y libgtk-4-dev libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad gstreamer1.0-libav \ gstreamer1.0-gtk4 libwebkitgtk-6.0-dev libssl-dev -# Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env -# Build -cd ~/betterframe/kiosk +cd /opt/betterframe/kiosk cargo build --release sudo install -Dm755 target/release/betterframe-kiosk /opt/betterframe/kiosk/betterframe-kiosk -# Install systemd user unit mkdir -p ~/.config/systemd/user -cp deploy/systemd/betterframe-kiosk.service ~/.config/systemd/user/ +cp /opt/betterframe/deploy/systemd/betterframe-kiosk.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now betterframe-kiosk ``` -### Angie proxy +Kiosks should point at the proxy URL, not direct backend ports: ```bash -sudo apt install -y angie # or nginx -sudo cp deploy/angie/betterframe.conf /etc/angie/conf.d/ -sudo systemctl reload angie +BETTERFRAME_SERVER=http:// /opt/betterframe/kiosk/betterframe-kiosk ``` -The Angie config gates `/nrdp/*` with the admin session/API-key auth-check -endpoint and `/in/kiosk/*` with the kiosk Bearer-key auth-check endpoint. +## Native server mode -Access: `http:///setup` for first-run. Kiosks should use the proxy URL -(`http://` or `http://betterframe.local`), not direct backend ports. - -## Docker - -```bash -docker compose -f deploy/docker/docker-compose.yml up -d -``` - -Kiosk still runs natively on the Pi (needs Wayland/HDMI), not in Docker. -The Compose stack uses `deploy/angie/betterframe.docker.conf` and -`deploy/docker/sec-config.yaml` because service names, not `127.0.0.1`, are the -correct upstreams inside the Docker network. - -Access: `http:///setup` for first-run. - -## Production secrets - -For production, store the server key via `systemd-creds`: - -```bash -sudo systemd-creds encrypt --name=betterframe-secret \ - /etc/betterframe/secret.key.plain /etc/betterframe/secret.key -sudo chmod 0600 /etc/betterframe/secret.key -sudo chown root:root /etc/betterframe/secret.key -``` - -The systemd unit's `LoadCredential=` directive injects this into the -service's `$CREDENTIALS_DIRECTORY`. +Native server mode is for development only. Run it manually when debugging; do +not install host daemons for BetterFrame server, Angie, or Node-RED in +production. The Docker stack owns those services. diff --git a/deploy/angie/betterframe.conf b/deploy/angie/betterframe.conf deleted file mode 100644 index 53fe6c0..0000000 --- a/deploy/angie/betterframe.conf +++ /dev/null @@ -1,133 +0,0 @@ -# BetterFrame Angie/nginx config — routes admin, kiosk-api, ws, node-red. -# -# Place in /etc/angie/conf.d/betterframe.conf or /etc/nginx/conf.d/betterframe.conf -# Run on the Pi alongside the server. TLS termination here; backend services -# bind to 127.0.0.1; Angie/nginx is the only public HTTP edge. - -# Upstreams (BSB services) -upstream betterframe_admin { server 127.0.0.1:18080; keepalive 16; } -upstream betterframe_api { server 127.0.0.1:18081; keepalive 16; } -upstream betterframe_ws { server 127.0.0.1:18082; } -upstream betterframe_nodered { server 127.0.0.1:1880; keepalive 8; } - -# Rate limiting for public endpoints -limit_req_zone $binary_remote_addr zone=bf_public:10m rate=30r/s; - -server { - listen 80; - listen [::]:80; - server_name betterframe.local _; - - # In production: redirect to HTTPS - # return 301 https://$host$request_uri; - - # For now: serve plain HTTP - client_max_body_size 16M; - - # ---- Admin UI + admin API (session-authenticated) ---- - location /admin/ { - proxy_pass http://betterframe_admin; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; - proxy_set_header Connection ""; - } - - location = /admin { return 301 /admin/; } - location /setup { proxy_pass http://betterframe_admin; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } - location /auth/ { proxy_pass http://betterframe_admin; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } - location /static/ { proxy_pass http://betterframe_admin; } - - # ---- Kiosk REST API (Bearer kiosk-key) ---- - location /api/kiosk/ { - proxy_pass http://betterframe_api; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_http_version 1.1; - proxy_set_header Connection ""; - } - - location /api/pair/ { - # Rate-limit pairing initiate to deter brute force - limit_req zone=bf_public burst=10 nodelay; - proxy_pass http://betterframe_api; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - - # ---- Admin API (session-authenticated) ---- - location /api/admin/ { - proxy_pass http://betterframe_admin; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - - # ---- Live kiosk WebSocket channel ---- - location /ws/kiosk { - proxy_pass http://betterframe_ws; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_read_timeout 86400s; # long-lived - proxy_send_timeout 86400s; - } - - # ---- Node-RED dashboard (admin-only) ---- - location /nrdp/ { - auth_request /api/admin/_check; - rewrite ^/nrdp/(.*) /$1 break; - proxy_pass http://betterframe_nodered; - proxy_set_header Host $host; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } - - # ---- Node-RED HTTP-in (public, rate-limited) ---- - location /in/public/ { - limit_req zone=bf_public burst=20 nodelay; - rewrite ^/in/public/(.*) /public/$1 break; - proxy_pass http://betterframe_nodered; - } - - # ---- Node-RED HTTP-in (kiosk-gated) ---- - location /in/kiosk/ { - auth_request /api/kiosk/_check; - rewrite ^/in/kiosk/(.*) /kiosk/$1 break; - proxy_pass http://betterframe_nodered; - } - - # ---- Proxy auth subrequests ---- - location = /api/admin/_check { - internal; - proxy_pass http://betterframe_admin; - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - proxy_set_header Cookie $http_cookie; - proxy_set_header Authorization $http_authorization; - proxy_set_header X-Real-IP $remote_addr; - } - - location = /api/kiosk/_check { - internal; - proxy_pass http://betterframe_api; - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - proxy_set_header Authorization $http_authorization; - proxy_set_header X-Real-IP $remote_addr; - } - - # ---- Health/readiness/version (public) ---- - location ~ ^/(healthz|readyz|version)$ { - proxy_pass http://betterframe_admin; - } - - # ---- Root redirect ---- - location = / { - proxy_pass http://betterframe_admin; - } -} diff --git a/deploy/angie/betterframe.docker.conf b/deploy/angie/betterframe.docker.conf index 886ee9b..96fb8e5 100644 --- a/deploy/angie/betterframe.docker.conf +++ b/deploy/angie/betterframe.docker.conf @@ -1,7 +1,7 @@ # BetterFrame Docker nginx config. # -# Same routes as betterframe.conf, but upstreams use compose service names -# instead of localhost. +# Production proxy routes for the Docker Compose stack. Upstreams use compose +# service names and only this proxy is published on the host. upstream betterframe_admin { server server:18080; keepalive 16; } upstream betterframe_api { server server:18081; keepalive 16; } diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 2272102..04a659b 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -1,15 +1,15 @@ -# BetterFrame stack — server + Angie proxy + Node-RED. +# BetterFrame stack: server + Angie proxy + Node-RED. # Kiosk runs on the Pi natively (not in Docker, needs Wayland/HDMI). # # Usage: -# docker compose -f deploy/docker/docker-compose.yml up -d +# docker compose -f deploy/docker/docker-compose.yml up -d --build # # Volumes: -# betterframe-data — sqlite DB + secret.key -# nodered-data — Node-RED flows +# betterframe-data: sqlite DB + secret.key +# nodered-data: Node-RED flows # -# Bind 0.0.0.0:80 on the host (Angie). Backend services only reachable -# from within the Docker network. +# Only 0.0.0.0:80 is published on the host. Backend services and Node-RED +# are reachable only from within the Docker network. version: "3.8" services: diff --git a/deploy/nodered/settings.js b/deploy/nodered/settings.js deleted file mode 100644 index 3a21367..0000000 --- a/deploy/nodered/settings.js +++ /dev/null @@ -1,11 +0,0 @@ -const settings = { - uiHost: "127.0.0.1", - uiPort: Number(process.env.PORT || 1880), - functionGlobalContext: {}, -}; - -if (process.env.NODE_RED_CREDENTIAL_SECRET) { - settings.credentialSecret = process.env.NODE_RED_CREDENTIAL_SECRET; -} - -module.exports = settings; diff --git a/deploy/systemd/betterframe-nodered.service b/deploy/systemd/betterframe-nodered.service deleted file mode 100644 index 7895425..0000000 --- a/deploy/systemd/betterframe-nodered.service +++ /dev/null @@ -1,27 +0,0 @@ -[Unit] -Description=BetterFrame Node-RED -Documentation=https://github.com/BetterCorp/BetterFrame -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=betterframe -Group=betterframe -WorkingDirectory=/var/lib/betterframe/nodered -Environment=NODE_ENV=production -Environment=PORT=1880 -ExecStart=/usr/bin/env node-red --userDir /var/lib/betterframe/nodered --settings /opt/betterframe/deploy/nodered/settings.js -Restart=on-failure -RestartSec=5 -StandardOutput=journal -StandardError=journal - -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -PrivateTmp=true -ReadWritePaths=/var/lib/betterframe/nodered - -[Install] -WantedBy=multi-user.target diff --git a/deploy/systemd/betterframe-server.service b/deploy/systemd/betterframe-server.service deleted file mode 100644 index 5d34cb3..0000000 --- a/deploy/systemd/betterframe-server.service +++ /dev/null @@ -1,29 +0,0 @@ -[Unit] -Description=BetterFrame Server (BSB) -Documentation=https://github.com/BetterCorp/BetterFrame -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=betterframe -Group=betterframe -WorkingDirectory=/opt/betterframe/server -Environment=NODE_ENV=production -Environment=NODE_OPTIONS=--import tsx -ExecStart=/usr/bin/node --import tsx /opt/betterframe/node_modules/@bsb/base/lib/scripts/bsb-plugin-cli.js start -Restart=on-failure -RestartSec=5 -StandardOutput=journal -StandardError=journal - -# Security hardening -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -PrivateTmp=true -ReadWritePaths=/var/lib/betterframe /var/log/betterframe -LoadCredential=betterframe-secret:/etc/betterframe/secret.key - -[Install] -WantedBy=multi-user.target