--- name: bnna-deploy-service version: 1.1.0 description: Deploy a service on an Alpine LXC in the bnna cluster behind the TLS router tags: [bnna, alpine, tls-router, deploy, service, webi, serviceman] --- # Deploy a Service (BNNA) ## BNNA TLS Router See `bnna-tls-router` skill for the full port/ALPN reference and IP derivation. Key facts for service deployment: - **Web services must listen on `:3080`** — the TLS router terminates HTTPS and forwards there. - The container does not need a TLS cert. - Access externally at `https://tls-.a.bnna.net` ```sh # Correct — reachable at https://tls-10-11-8-x.a.bnna.net BNNA_ADDR=:3080 ``` ## First Boot: Create the App User (run once as root) First detect the OS: ```sh ssh root@ 'cat /etc/issue' ``` ### Debian / Ubuntu (apt) ```sh ssh root@ 'apt-get update && apt-get install -y sudo curl unzip git' ssh root@ 'curl -sS https://webi.sh/webi | sh && source ~/.config/envman/PATH.env && webi ssh-utils' ``` ### Alpine (apk) ```sh ssh root@ 'apk add --no-cache sudo curl' ssh root@ 'curl -sS https://webi.sh/webi | sh && ~/.local/bin/webi webi-essentials ssh-utils' ``` Then create the app user (all distros). Non-interactive SSH sessions may not load the shell profile, so source PATH explicitly: ```sh ssh root@ 'source ~/.config/envman/PATH.env && ssh-adduser app' ``` `ssh-adduser` creates the user, copies your public key to `~app/.ssh/authorized_keys`, and grants passwordless sudo. After this, use `app@` for all connections. ## Service User Convention Each single-purpose LXC runs its service as a user named **`app`**. Root is only used for system-level setup (package installs, OpenRC registration, logrotate). Standard path layout under the app user's home: ``` ~/bin/ # binary ~/.config//current.env # environment / secrets ~/.local/share// # runtime state ``` Config and secrets live in the app user's home — **not** `/etc//`: ``` /home/app/.config//env # environment / secrets /home/app/.config//users.tsv # auth credentials (if applicable) ``` Lock down permissions: ```sh chmod 700 /home/app/.config// chmod 600 /home/app/.config//env chown -R app:app /home/app/.config// ``` ## Install via Webi Install webi as the service user (or root for system tools): ```sh curl -sS https://webi.sh | sh source ~/.config/envman/PATH.env ``` Then install the service binary and any dependencies via webi: ```sh webi ``` Binaries land in `~/.local/bin/` (or `~/.local/opt//bin/` for larger packages). ## Environment / Config Files Service config and secrets go in `~/.config//current.env`: ```sh install -m 600 /dev/null ~/.config/myservice/current.env cat > ~/.config/myservice/current.env <<'EOF' BNNA_ADDR=:3080 DATABASE_URL=postgres://... EOF ``` Pass the env file to the binary at startup (see serviceman example below), or source it in the init script. Update config without redeploying the binary: ```sh scp .env app@:~/.config/myservice/current.env ssh app@ 'chmod 600 ~/.config/myservice/current.env && sudo systemctl restart myservice' ``` ## Registering a Boot Service Use `serviceman` to register the service — it handles both OpenRC (Alpine) and systemd (Debian) automatically: ```sh webi serviceman serviceman add --name myservice --envfile ~/.config/myservice/current.env -- /path/to/binary --flag value ``` Occasionally there are system-specific extras to handle after registration. For example: - **Debian LXC + systemd**: containers may need config tweaks that systemd expects but OpenRC does not (e.g. `dynamic_shared_memory_type = mmap` for PostgreSQL) - **Alpine / OpenRC**: some packages may need additional `depend()` hooks or service-user setup outside of what serviceman generates ## Logrotate (Alpine) Add a logrotate rule for service logs: ``` /var/log/myservice/*.log { daily rotate 14 compress missingok notifempty postrotate rc-service myservice reload > /dev/null 2>&1 || true endscript } ``` Place in `/etc/logrotate.d/myservice`. ## Node.js Services For Node.js services, use the `--envfile` flag with Node 20.6+: ```sh ssh app@ '. ~/.config/envman/PATH.env && cd ~/srv/ && serviceman add --name -- ~/.local/opt/node/bin/node --envfile ~/.config//current.env ~/server/src/index.js' ``` **Important:** Node.js services need `PORT=3080` in the env file for the TLS router. ### Private NPM Packages For `@sheet/edit` and other private packages, create `.npmrc`: ```sh ssh app@ 'cat > ~/server/.npmrc << '\''EOF'\'' @sheet:registry=https://pylon.sheetjs.com:54111/ //pylon.sheetjs.com:54111/:_authToken="" EOF' ``` ## Database Setup For dev environments that need a local database: ### MariaDB (local) ```sh ssh app@ 'source ~/.config/envman/PATH.env && webi mariadb@11.4' ssh app@ 'source ~/.config/envman/PATH.env && serviceman add --name mariadb --workdir ~/.local/opt/mariadb/ -- mariadbd --defaults-file=/home/app/.my.cnf' ssh app@ 'mariadb -S ~/.local/share/mariadb/run/mariadbd.sock -e "CREATE DATABASE IF NOT EXISTS mydb; CREATE USER IF NOT EXISTS myuser@localhost IDENTIFIED BY '\''mypass'\''; GRANT ALL ON mydb.* TO myuser@localhost"' ``` Update env file: `DB_URL=mysql://myuser:mypass@localhost:3306/mydb` ### Copy Database from Staging Use SSH tunnel to staging database, dump, transfer, and restore: ```sh # 1. Start tunnel (assumes paperos-staging-db in ~/.ssh/config) ssh -f -N paperos-staging-db # 2. Dump database mysqldump -u -p'' -h 127.0.0.1 -P 43306 --single-transaction --routines | gzip > /tmp/-backup.sql.gz # 3. Transfer and restore scp /tmp/-backup.sql.gz app@:/tmp/ ssh app@ 'gunzip -c /tmp/-backup.sql.gz | mariadb -S ~/.local/share/mariadb/run/mariadbd.sock ' ```