How to put a specific partner branch of vuer_oss / vuer_css onto the fk-dev VM: file ownership, the read-only per-service config/local.json, the per-partner database, operator users, and the client-asset build. Companion to fk-dev-deploy-smoke-runbook — that note owns the smoke half (janus/WebRTC fix, OpenHours, password reset, videochat gotchas); this note owns the partner swap half. Every fact below was observed live on fk-dev on 2026-08-18 while deploying vuer_css fix/FKITDEV-8931-socket-test + vuer_oss customization/mkb-instant.
Connect as root:command ssh -A root@fk-dev. command ssh (not bare ssh) is mandatory — see dev-build-host.
Order matters more than the commands do. Restarting with the new partner’s code but the old partner’s db.urlsilently migrates the wrong database — see the syncOnStart hazard. Follow Deploy recipe in order.
Deploying a partner ≠ checking out a branch. It is: branch + config/local.json rewrite + a vuer_oss_<partner> database + operator users + (for css) yarn build.
Host has no node and no yarn — everything install/build goes through docker exec.
root and ops are both permitted (verified 2026-08-18 with root, which was used for the whole deployment):
tailscale up # local Tailscale must be runningcommand ssh -A root@fk-dev
Refused by tailnet policy with tailnet policy does not permit you to SSH as user "<u>": levander, facekom, vuer, ubuntu, dev, techteamer, admin.
SSH-user reconciliation across notes — both work
dev-build-host (2026-08-12) records both ops and root; fk-dev-deploy-smoke-runbook (2026-08-13) recorded ops only. The 2026-08-18 deploy ran entirely as root. Settled: ops and root both work, everything else is refused.
The choice is not cosmetic:
ops has no sudo — anything root-owned (or any chown) must go through docker exec -u 0:0.
root writes root-owned files, which then need fixing up per the UID trap.
The per-service tailnet names are NOT SSH hosts
oss-fk-dev (100.91.55.42), css-fk-dev (100.106.240.87), portal-fk-dev, esign-*-fk-dev, css-sdk-demo-fk-dev are Tailscale sidecar containers. Port 22 is refused there — they serve HTTPS only. All shell work happens on fk-dev itself.
GitHub from the box
fk-dev has no GitHub credentials of its own (git@github.com: Permission denied (publickey)). Forward your agent:
# on the Macssh-add --apple-load-keychain # loads id_ed25519 (authenticates to GitHub as wowjeeez)command ssh -A root@fk-dev# on the boxexport GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new"
An earlier session solved this differently, by scp-ing a git bundle — the reflog still shows a since-removed bundle remote (refs/remotes/bundle/FKITDEV-9059). Agent forwarding is the simpler path.
Architecture on the box
Property
Value
Compose project
/workspace/vuer_docker (overlay documented in /workspace/vuer_docker/tailscale/README.md)
Access path
one kernel-mode tailscaled sidecar per service running tailscale serve, terminating HTTPS on <prefix>-fk-dev.taild4189d.ts.net. nginx_proxy from dev.yml is NOT the access path.
supervisord -n -c /etc/supervisor/supervisord.conf is each app container’s CMD; per-service conf at /etc/supervisor/conf.d/supervisor_vuer_<svc>_dev.conf, autorestart=true
App identity
user techteamer, NODE_ENV=dev, DEV_DOMAIN=fk-dev.taild4189d.ts.net, cwd /workspace/vuer_<svc>, command node server.js
Ports
vuer_oss web 10081 / socket 10080; vuer_css web 10083 / socket 10082
Source
bind-mounted from the host: /workspace/vuer_oss, /workspace/vuer_css (host owner ops)
Logs
bind-mounted to /workspace/vuer_docker/workspace/log/<svc>
Node / Yarn
inside containers only — node v24.12.0, yarn 1.22.22. The host has neither.
One file per service, not per partner. There is no …-mkb-instant-local.json.
It survives branch switches.git checkout cannot change it, so switching partner means rewriting it by hand on the host.
It is a single-file bind mount, so docker binds the inode. Any edit that replaces the file replaces the inode, and the container keeps reading the old one.
The four traps
1. Trap — UID mismatch: host ops is 1001, container techteamer is 1000
A tree-wide chown breaks logging — the two commands are a pair, never run only the first
Host ops is uid 1001. In-container techteamer is uid 1000, which on the host maps to ubuntu. The tree belongs to ops; logs/ belongs to 1000.
After installing or building as root, run both halves, per repo:
The untouched sibling repo is your ownership reference
If you are unsure what a path should be owned by, stat the repo you have not touched yet:
ls -ld /workspace/vuer_css/logs # ubuntu ubuntu ⇒ uid 1000
"Ownership moved to 1001, the uid-1000 guidance is stale" — this is FALSE, and it is an agent artefact, not doc rot
The two uids coexist; they do not contradict each other. The working tree is 1001:1002 (host ops) and the app process plus logs/ are 1000 (techteamer in-container = ubuntu on host). Both statements are true at the same time, and both are current.
Traced provenance (2026-08-18): the claim appears in no vault note — a full-vault search found every note that touches uids stating 1000 correctly. It was generated by a historian subagent that observed the tree live as 1001:1002, correctly, and then over-generalised that single observation into “this contradicts the FKITDEV-9059 note, ownership has since changed” and “the 9059-era uid-1000 guidance is stale.” It was then repeated across an agent hand-off. Acting on it cost a supervisor restart loop.
FKITDEV-9059.md:78 was right the whole time
"Container user techteamer is uid 1000 = host ubuntu, not host ops (1001)." Written 2026-07-20, still accurate. It was never stale and should not be read with suspicion. release-pipeline-automation-spec says the same.
What to be on guard against is the mechanism, not the fact. An agent reporting a live ls -l and concluding “the docs are out of date” is the failure mode here — a partial observation promoted to a correction. Trust FKITDEV-9059 and this runbook equally, and treat any hand-off claiming the uid guidance is outdated as suspect until it explains both uids.
2. Trap — yarn install as uid 1001 fails, and piping to tail hides it
Run it as root in-container, and never pipe it
Running yarn install as ops (uid 1001) dies with EACCES on pre-existing node_modules paths, e.g.
This is the same “green wrapper masks a failed step” trap that made a cofidis smoke over-claim in FKITDEV-9059, where bin/build/build.js exited 0 after every write had hit EACCES.
3. Trap — the mounted local.json is per-service, survives checkout, and is inode-bound
It still holds the previous partner
The mounted vuer_oss-local.json was found still carrying the previous tenant’s (CIB)db.urland a 30-entry flow.flows list. Left in place on a different partner’s branch it points the app at the wrong database (see Trap 4 for why that is worse than it sounds) and registers flows that do not exist on that branch.
But you cannot simply delete the file. config/dev.json ships hosts as empty strings, and janus as wss://localhost:8989 while fk-dev’s janus actually listens on ws://localhost:8188 (admin ws://localhost:7188 — the plain-ws switch is the janus fix).
Rule: hosts and webrtc.janusServers in local.json are mandatory box infrastructure — keep them. Drop only the partner-specific keys (db.url, flow.flows, and anything else tenant-shaped).
Rewrite it with cat > only — docker binds the inode
config/local.json is a single-file bind mount. sed -i, mv, and git checkout -- all replace the inode, and the container silently goes on reading the old file — no error, no warning, config changes that appear to do nothing. This has burned two prior sessions.
4. Trap — db.syncOnStart migrates whatever db.url currently points at
A restart in the wrong order silently migrates the previous partner's database
config/dev.json sets db.syncOnStart: true, and server/bootstrap/connection/db.js runs migrate → sync → migrate on every boot. So a boot with the new partner’s code checked out and the old partner’s db.url still in local.json applies the new branch’s migrations to the old partner’s database.
Supervisor’s autorestart=true means you do not even have to restart deliberately — a crash anywhere in the checkout window is enough to trigger it.
Therefore: stop the app programs, create the new DB, and rewrite local.json BEFORE the checkout. That is why Deploy recipe is ordered the way it is.
Verified clean on 2026-08-18 — no cross-migration occurred
Audited after the fact: vuer_oss_cib holds 158 migrations, 5 CIB-only, and zero mkb-only entries. Migration-count comparison is the reliable isolation check on this box (the same technique that confirmed the cofidis repoint in release-pipeline-automation-spec).
Existing automation — vuer.sh
/workspace/vuer_docker/bin/vuer.sh already automates part of this. Its checkout path runs:
Hardcoded, stale branch list — it offers customization/mkb, not customization/mkb-instant.
No db.url handling at all — it does nothing about Trap 4, which is the one that can corrupt another partner’s database.
It builds AFTER starting the server (server_start → dev_build), so the box serves stale assets for the length of the build. The recipe below builds first.
vuer.sh db init seeds admin and operator with password = username.
Deploy recipe
Repos on disk are ops-owned while you are root, so every git command needs an explicit safe.directory:
Per-partner convention: vuer_oss_<partner>. Databases present on the box as of 2026-08-18: vuer_oss, vuer_oss_cib, vuer_oss_cofidis, vuer_oss_test (+ the new vuer_oss_mkb_instant).
Without -h, peer authentication fails. With -h localhost plus PGPASSWORD=dev -U dev it connects. (The concrete workaround for the “peer-auth blocks psql -U postgres” note in fk-dev-nusz-deploy-and-8959-verification.)
No manual migration step is needed — db.syncOnStart will run the full chain on first boot in step 6. Observed end-to-end against a brand-new empty database.
3. Rewrite the partner config
Edit /workspace/vuer_docker/tailscale/config/vuer_<svc>-local.json on the host (it is read-only inside the container), with cat > / heredoc only — see the inode warning in Trap 3. Keep hosts + webrtc.janusServers; set db.url to the new partner’s database; drop the old flow.flows.
The explicit refspec is mandatory — facekom clones use narrowed fetch refspecs and a plain git fetch origin <branch> leaves origin/<branch>stale while exiting 0 (narrowed-fetch-refspec-stale-devel-merge).
Any client-side change is invisible until yarn build runs
yarn build = bin/build/build.js = the external + script + style + pdfjs tasks (~10s). Output lands in gitignored directories: /workspace/vuer_css/web/{js,css,branding,polyfills,libs/pdfjs/wasm}, and nginx serves them from root /workspace/vuer_css/web. Checking out a branch alone changes nothing the browser can see.
Build beforestart (unlike vuer.sh) so the box never serves stale assets. And per FKITDEV-9059, the build wrapper can exit 0 having written nothing if web/ ownership is wrong — so verify the served asset, not the exit code (Verification checklist).
Roles are defined in config/roles.json: admin, supervisor, operator. (vuer.sh db init is the alternative — it seeds admin/operator with password = username.)
The CLI takes only ONE role — the house convention is all three
bin/db/create_user accepts a single role. Grant the full set with a direct update:
update users set rights = '["admin","supervisor","operator"]' where username = 'admin';
Check whether the partner ships a password-policy listener before blaming the password
Partners can add customization/listeners/* hooks that reject logins. cofidis’s verifypassword.js pins a fixed password and rejects admin (FKITDEV-9059). mkb-instant’s verify-password-test.js is harmless — it only rejects the literal string incorrectAccordingToPasswordPolicy, so it is not a login blocker.
Verification checklist
Supervisor — every program RUNNING with non-zero uptime:
docker exec <container> supervisorctl status
Uptime 0:00:00 with a changing PID = restart loop, and supervisorctl says RUNNING either way
RUNNING is not the signal — uptime is. A healthy service accumulates uptime; a looping one is re-spawned faster than you can read it, so uptime stays pinned at 0:00:00 while the pid changes between calls. Run supervisorctl status twice and compare the pid. On 2026-08-18 this was the EACCES … logs/server.log of Trap 1.
Logs show both listeners:
Web server is listening on 1008xandSocket server is listening on 1008x.
HTTPS from the Mac (fk-dev cannot resolve its own sidecar MagicDNS non-interactively):
Grep the bundle as SERVED over HTTPS, not the file on disk — this is the only check that distinguishes “branch checked out” from “browser gets the new code”:
Assert the referenced assets return 200, not just the HTML.
HTML 200 with every stylesheet 404 is a real, observed failure mode still exited 0 (FKITDEV-9059). Pull the asset URLs out of the HTML and check each one.
A prior session had a 200 page with correct partner markers while every stylesheet 404’d, because the build had written nothing and
Success redirects to /; failure redirects back to /login.
Two false alarms — do not re-chase these
(a) An old mtime on web/branding/layouts means nothing
The directory mtime is stale because the .branding.css files inside are overwritten in place; the files carry the real build time (verified: all Aug 18 08:57). Stat the files, not the directory.
(b) mkb-instant's verify-password-test.js is not a login blocker
It rejects only the literal string incorrectAccordingToPasswordPolicy. Contrast cofidis’s verifypassword.js, which really does pin a fixed password (FKITDEV-9059).
Rollback — /workspace/_restore/
The box keeps prior-tenant state under /workspace/_restore/ so a partner swap is reversible:
nusz-state-*.txt, vuer_oss-local.json.nusz.bak, and a janus-fix patch (from earlier rounds)
Follow the precedent: before swapping partners, drop a <partner>-state-<YYYYMMDD-HHMM>.txt (branch + commit per repo, DB name, login) and back up both vuer_{oss,css}-local.json there. Restore the config with cat bak > file — never mv, per the inode warning in Trap 3.
Box state left by the 2026-08-18 deploy
vuer_css
fix/FKITDEV-8931-socket-test @ 3f32334ae
vuer_oss
customization/mkb-instant @ c7ceaac2da
Database
vuer_oss_mkb_instant
Login
admin / Facekom123!
Previously
cib-9197 against vuer_oss_cib — restorable from /workspace/_restore/
kiosk.layout and videochat still hard-reload — that is out of scope for FKITDEV-8931, not a regression. mkb-instant is heavily kiosk-based, so testing on a kiosk page will look like a failure.
Correct test target:https://css-fk-dev.taild4189d.ts.net/ lands on mbh-services, which extends default.layout and is the non-kiosk DÁP path.
Reconnect timing, the patch itself, and the do-not-cherry-pick warning are in FKITDEV-8931.
Browser-tested 2026-08-18 — a long outage still reloads the page, and the threshold is random
The short-drop case passes (~1.4s → silent re-auth, no reload). But the fix only cancels a pre-existing reload countdown; it does not remove it. A 40s outage (supervisorctl stop vuer_css) hard-reloaded the page 4× against a 502. The countdown is hideDelay: 10 + random(50) ⇒ 10–60s, re-rolled on every page load, so this reproduces intermittently.
Also: connectionStateRecovery never engages here (socket.recovered === false on every reconnect) — do not test against a 30s boundary. Full results in FKITDEV-8931.
release-pipeline-automation-spec — independently records the syncOnStart ordering hazard, the inode bind-mount trap, and the uid 1000/1001 split
FKITDEV-9059 — the cofidis repoint: uid-1000-vs-web-1001, the build wrapper that exits 0 having written nothing, and the verifypassword.js login blocker