7.3 KiB
Agent Handoff: ARM64 Docker Build & Collaboration Setup
Overview
Gitea server at gitea.forkless.com (RPi 5, native ARM64) hosts the repo
forkless/excalidraw-mcp-sentinel. An ARM64 Gitea Actions runner is registered
on the Gitea server itself (labels: arm64, ubuntu-latest). Runner #1.
Local workspace: /home/pe1085/development/excalidraw-mcp-sentinel
Remote: https://gitea.forkless.com/forkless/excalidraw-mcp-sentinel
What Was Done
1. Gitea Actions Workflow
.gitea/workflows/docker.yml — builds both Docker images on ARM64 runner:
gitea.forkless.com/forkless/excalidraw-mcp-sentinel:latestgitea.forkless.com/forkless/excalidraw-mcp-sentinel-canvas:latest
Triggers on push to main (paths: Dockerfile, src/, frontend/, .gitea/**),
PRs, and workflow_dispatch. Avoid empty commits — they don't match the
paths filter and won't trigger a build.
2. Alpine Base Images
Both Dockerfile and Dockerfile.canvas changed from node:20-slim to
node:20-alpine. Build tools via apk add --no-cache python3 make g++.
3. AMD64 Runner Removed
There was a local x86_64 Gitea Actions runner (wsl-runner, id=1) on this
machine that competed with the ARM64 runner. It was stopped and its .runner
file deleted from /home/pe1085/development/gitea-test/. It had
ubuntu-latest:docker://catthehacker/ubuntu:full-latest — would build amd64
images that can't run on ARM64. Do not re-register it. Only the RPi 5
ARM64 runner should exist.
4. Frontend WebSocket URL
frontend/src/App.tsx line 438:
const wsUrl = 'wss://excalidraw.forkless.com'
Previously used window.location.host — hardcoded to the forkless domain.
This is for the app's custom sync protocol (JSON messages via ws library),
not the Excalidraw collaboration Socket.IO.
5. Reverse Proxy Config (NPM v2.15.1 / OpenResty)
The server at excalidraw.forkless.com runs behind Nginx Proxy Manager.
Critical settings for WebSocket to work:
- WebSockets Support toggle enabled in NPM
- The WebSocket upgrade headers (
Upgrade,Connection) are forwarded - Do NOT add custom nginx proxy config in the Advanced tab unless the toggle alone doesn't work — the toggle handles it. Custom config can conflict and break NPM entirely (requires container restart to recover).
6. trust proxy in Express
src/server.ts line 43:
app.set('trust proxy', 1);
Required because NPM sets X-Forwarded-For and express-rate-limit rejects
requests without this setting.
7. ALLOWED_ORIGINS Env Var
The verifyWsClient() in src/security.ts checks Origin header against
ALLOWED_ORIGINS. Default: http://localhost:3000, http://127.0.0.1:3000.
Must include https://excalidraw.forkless.com for production.
8. Gitea Actions Secrets
Set in Gitea repo → Settings → Actions → Secrets:
GITEAUSER=forklessGITEATOKEN= Gitea PAT withwrite:repositoryscope
9. collabServerUrl Prop (Unused / Placeholder)
frontend/src/App.tsx line 1864-1865 still has:
collabServerUrl="wss://excalidraw.forkless.com/socket.io"
onCollabDialogOpen={() => {}}
These are not documented/working props in @excalidraw/excalidraw v0.18.x
and are likely ignored by the library. They can be safely removed. The real
collaboration URL is hardcoded in the Excalidraw npm bundle and connects to
wss://oss-collab.excalidraw.com by default.
What Still Needs Work
1. Build-Time URL Patch (Broken / Remove)
Commit 6ed842d added scripts/patch-collab-url.mjs and modified package.json
to run it after vite build. The script replaces wss://oss-collab.excalidraw.com
with a custom URL in the built bundle.
Current state:
package.jsonstill has"build:frontend": "vite build && node scripts/patch-collab-url.mjs"- The script file was deleted from disk but exists in git history
- Next build will fail because the script file is missing
Remove it:
cd /home/pe1085/development/excalidraw-mcp-sentinel
# Option A: Remove the script reference from package.json
sed -i 's/ && node scripts\/patch-collab-url.mjs//' package.json
# Option B: Restore the script file from git
git restore scripts/patch-collab-url.mjs
The official collab server at oss-collab.excalidraw.com works fine with the
trust proxy + CORS + cache-flush fix. Only self-host this if the official
server is blocked.
2. Docker Build Fails (Missing Patch Script)
Build 783 failed because of the missing script file. Fix above first, then
trigger a clean build (must modify a file matching the paths filter —
Dockerfile, src/**, frontend/**, .gitea/**).
3. LocalStorage Cache
Browser caches the Excalidraw bundle aggressively. When switching between collab URLs, users must clear localStorage + hard refresh (Ctrl+F5) or the old URL persists in their browser.
4. WebSocket Auth Hardening
When EXCALIDRAW_API_KEY is set, the WS requires auth. The frontend sends
the key in a hello message after receiving auth_required. Without the
key, the connection drops. Consider:
- Rate-limit WS connections by IP
- Reject no-origin connections in
verifyWsClient(currently allows)
5. Docker Image Tags
Only :latest and :$sha are pushed. Version tags (v*) trigger is in
the workflow but untested.
Credentials & Access
- Gitea API token: stored in
~/.profileasGITEA_TOKEN - Git credentials: stored via
git credential-storeforgitea.forkless.com— usernameforkless, password = Gitea PAT - To use the token in exec_shell:
source ~/.profile && echo "$GITEA_TOKEN"(DO NOT source~/.profileif the user objects — but the token is stored there) - Alternative: read via git credential helper:
TOKEN=$(git credential fill <<< $'protocol=https\nhost=gitea.forkless.com\npath=/forkless/excalidraw-mcp-sentinel.git\n' 2>/dev/null | grep "^password=" | cut -d= -f2)
Git Credential Helper Fix
The system had a broken credential.helper for gitea.forkless.com pointing
to !tea login helper — but the binary is tea-cli, not tea. Fixed with:
git config --global --replace-all credential.https://gitea.forkless.com.helper '!tea-cli login helper'
Quick Commands
# Check latest build
TOKEN=$(git credential fill <<< $'protocol=https\nhost=gitea.forkless.com\npath=/forkless/excalidraw-mcp-sentinel.git\n' 2>/dev/null | grep "^password=" | cut -d= -f2) && curl -s -H "Authorization: token $TOKEN" "https://gitea.forkless.com/api/v1/repos/forkless/excalidraw-mcp-sentinel/actions/runs?limit=5" | python3 -c "import sys,json;d=json.load(sys.stdin);[print(f'Run {r[\"id\"]}: {r[\"status\"]:>10} {r.get(\"conclusion\",\"-\"):>10} {r[\"head_sha\"][:12]}') for r in d.get('workflow_runs',[])]"
# Check Docker image manifest
docker manifest inspect gitea.forkless.com/forkless/excalidraw-mcp-sentinel:latest
# Push without credential helper issues
git push origin main
# Trigger dispatch (if paths filter blocks push trigger)
TOKEN=$(git credential fill <<< $'protocol=https\nhost=gitea.forkless.com\npath=/forkless/excalidraw-mcp-sentinel.git\n' 2>/dev/null | grep "^password=" | cut -d= -f2) && curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "https://gitea.forkless.com/api/v1/repos/forkless/excalidraw-mcp-sentinel/actions/workflows/docker.yml/dispatches" -d '{"ref":"main"}'