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.
This commit is contained in:
Mitchell R 2026-05-11 10:08:33 +02:00
parent 96d7cc45ba
commit 02412169a0
No known key found for this signature in database
7 changed files with 56 additions and 277 deletions

View file

@ -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://<pi-ip>/setup
```
Node-RED is reachable only through:
```text
http://<pi-ip>/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://<pi-ip> /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://<pi-ip>/setup` for first-run. Kiosks should use the proxy URL
(`http://<pi-ip>` 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://<pi-ip>/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.

View file

@ -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;
}
}

View file

@ -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; }

View file

@ -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:

View file

@ -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;

View file

@ -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

View file

@ -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