CI/CD + deploy: Gitea Actions -> registry -> Dockge -> NPM (rcai.brasse-pc.eu)
Some checks failed
build-and-push / build (push) Has been cancelled
Some checks failed
build-and-push / build (push) Has been cancelled
Server:
- serverar byggd web-frontend statiskt + SPA-fallback (WEB_DIST)
- host-mountad persistent katalog via DATA_DIR (/srv/docker/agent-helm -> /app/data),
förberedd för framtida db/auth enligt övriga appars konvention
- esbuild-bundle -> dist/server.cjs (en fil, inga node_modules i runtime)
Web:
- WS-URL defaultar till samma origin i prod (wss://rcai.brasse-pc.eu), localhost i dev
Deploy-artefakter:
- Dockerfile: multi-stage, bygger web + bundlar server; --filter utesluter daemon
(ingen node-pty-kompilering); kör som uid 1000 (= brasse på host)
- .gitea/workflows/build.yaml: push master -> bygg arm64 -> push localhost:5000
- deploy/act-runner.{compose,config}.yaml: Gitea Actions-runner i Docker (docker.sock,
job-containrar får värdens docker for build/push)
- deploy/agent-helm.compose.yaml: Dockge-stack (srv_default, expose 8787, data-mount,
x-dockge.urls)
- deploy/DEPLOY.md: full runbook (runner, bygge, stack, NPM, uppdatering, felsökning)
Verifierat lokalt: typecheck rent, server-bundle serverar UI+health+assets+SPA-fallback,
data-dir skapas. Docker-bygget validerat så långt daemonen är nere (filter bekräftat).
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
.git
|
||||
.gitea
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.log
|
||||
deploy
|
||||
README.md
|
||||
27
.gitea/workflows/build.yaml
Normal file
27
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: build-and-push
|
||||
|
||||
# Bygger image:n på Pi5-runnern (arm64, nativt) och pushar till den lokala
|
||||
# registryn. localhost:5000 trustas automatiskt av Docker (ingen insecure-config).
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
docker build \
|
||||
-t localhost:5000/agent-helm-server:latest \
|
||||
-t "localhost:5000/agent-helm-server:${GITHUB_SHA::12}" \
|
||||
.
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push localhost:5000/agent-helm-server:latest
|
||||
docker push "localhost:5000/agent-helm-server:${GITHUB_SHA::12}"
|
||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# agent-helm control-plane + web-UI i en enda image.
|
||||
# Byggs av Gitea Actions PÅ Pi5 (arm64) och pushas till localhost:5000.
|
||||
# Servern serverar den byggda Vue-frontenden statiskt + WebSocket på samma port.
|
||||
|
||||
# ---------- builder ----------
|
||||
FROM node:22-slim AS builder
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm@11.3.0
|
||||
|
||||
# Installera bara det server + web behöver. "..."-suffixet tar med workspace-
|
||||
# beroenden (shared) men UTESLUTER daemon -> ingen node-pty-kompilering här.
|
||||
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml .npmrc ./
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/server/package.json ./packages/server/
|
||||
COPY packages/web/package.json ./packages/web/
|
||||
COPY packages/daemon/package.json ./packages/daemon/
|
||||
RUN pnpm install --frozen-lockfile \
|
||||
--filter "@agent-helm/server..." \
|
||||
--filter "@agent-helm/web..."
|
||||
|
||||
# Källkod + bygg (web -> dist, server -> en bundlad cjs-fil)
|
||||
COPY packages/shared ./packages/shared
|
||||
COPY packages/server ./packages/server
|
||||
COPY packages/web ./packages/web
|
||||
RUN pnpm --filter @agent-helm/web build \
|
||||
&& pnpm --filter @agent-helm/server bundle
|
||||
|
||||
# ---------- runtime ----------
|
||||
FROM node:22-slim AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
PORT=8787 \
|
||||
WEB_DIST=/app/web
|
||||
COPY --from=builder /app/packages/server/dist/server.cjs ./server.cjs
|
||||
COPY --from=builder /app/packages/web/dist ./web
|
||||
EXPOSE 8787
|
||||
USER node
|
||||
CMD ["node", "server.cjs"]
|
||||
133
deploy/DEPLOY.md
Normal file
133
deploy/DEPLOY.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Deploy: agent-helm på Pi5 via Gitea Actions → registry → Dockge → NPM
|
||||
|
||||
Flödet: `git push master` → **Gitea Actions-runnern på Pi5** bygger arm64-image:n
|
||||
→ pushar till `localhost:5000/agent-helm-server:latest` → **Dockge** kör stacken →
|
||||
**NPM** exponerar `https://rcai.brasse-pc.eu`. Uppdatering = push + klicka *Update* i Dockge.
|
||||
|
||||
```
|
||||
push master ──► gitea-act-runner (bygg arm64) ──► localhost:5000/agent-helm-server:latest
|
||||
│
|
||||
Dockge: Update (pull + up -d)
|
||||
▼
|
||||
container "agent-helm" :8787 (srv_default)
|
||||
▲
|
||||
NPM: rcai.brasse-pc.eu ─┘ (Websockets ON, TLS)
|
||||
```
|
||||
|
||||
Förutsättningar som redan är på plats: DNS `rcai.brasse-pc.eu → 155.4.72.51` (✓),
|
||||
`srv_default`-nätet, `docker-registry` och `npm` körs (✓). Gitea 1.24 har Actions
|
||||
påslaget som default.
|
||||
|
||||
---
|
||||
|
||||
## Steg 1 — Registrera Gitea Actions-runnern (engång)
|
||||
|
||||
1. **Hämta registrerings-token** i Gitea:
|
||||
`Site Administration → Actions → Runners → Create new runner` → kopiera token.
|
||||
(Eller repo-nivå: `brasse/agent-helm → Settings → Actions → Runners`.)
|
||||
|
||||
2. **Skapa stacken i Dockge** (`dockge.brasse-pc.eu`):
|
||||
- Ny stack, namn `act-runner`.
|
||||
- Klistra in `deploy/act-runner.compose.yaml`.
|
||||
- Lägg `deploy/act-runner.config.yaml` som filen `config.yaml` i stacken
|
||||
(Dockge låter dig skapa extra filer i stack-mappen, dvs
|
||||
`/srv/dockge-staks/act-runner/config.yaml`).
|
||||
- I stackens **env**: `GITEA_RUNNER_REGISTRATION_TOKEN=<din-token>`.
|
||||
- Deploy.
|
||||
|
||||
3. **Verifiera**: i Gitea `Site Administration → Actions → Runners` ska
|
||||
`pi5-docker-runner` dyka upp som *Idle*. Loggen: `docker logs gitea-act-runner`.
|
||||
|
||||
> Runnern monterar värdens `docker.sock` och bygger med Pi5:ans egen Docker →
|
||||
> arm64 nativt, och `localhost:5000` (registryn) trustas automatiskt.
|
||||
|
||||
---
|
||||
|
||||
## Steg 2 — Första bygget
|
||||
|
||||
`.gitea/workflows/build.yaml` triggas på push till `master`/`main`. Trigga det:
|
||||
|
||||
- Pusha en commit till `master`, **eller**
|
||||
- Gitea repo → `Actions` → välj workflowen → `Run workflow` (manuell dispatch).
|
||||
|
||||
Följ körningen under repo:ts **Actions**-flik. När den är grön finns
|
||||
`localhost:5000/agent-helm-server:latest` i registryn.
|
||||
|
||||
Snabbkoll (på Pi5, eller via `claude-docker`):
|
||||
```bash
|
||||
curl -s http://localhost:5000/v2/agent-helm-server/tags/list
|
||||
# -> {"name":"agent-helm-server","tags":["latest","<sha>"]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Steg 3 — Kör stacken i Dockge
|
||||
|
||||
1. **Skapa host-katalogen för config/data** (som `brasse`, uid 1000):
|
||||
`mkdir -p /srv/docker/agent-helm`
|
||||
Den bind-mountas till `/app/data` i containern (där framtida db/auth hamnar);
|
||||
containern kör som uid 1000 = `brasse` så filerna blir korrekt ägda.
|
||||
2. Ny stack i Dockge, namn `agent-helm`.
|
||||
3. Klistra in `deploy/agent-helm.compose.yaml`.
|
||||
4. I stackens **env**: `AGENT_HELM_TOKEN=<lång-slumpad-hemlighet>`
|
||||
(generera t.ex. `openssl rand -hex 32`).
|
||||
5. Deploy. Kolla i Dockge att containern `agent-helm` är *running*.
|
||||
|
||||
---
|
||||
|
||||
## Steg 4 — NPM proxy host (manuellt i NPM-UI:t)
|
||||
|
||||
I `Proxy.brasse-pc.eu` → **Proxy Hosts → Add Proxy Host**:
|
||||
|
||||
| Fält | Värde |
|
||||
|---------------------|-----------------------------|
|
||||
| Domain Names | `rcai.brasse-pc.eu` |
|
||||
| Scheme | `http` |
|
||||
| Forward Hostname | `agent-helm` |
|
||||
| Forward Port | `8787` |
|
||||
| **Websockets Support** | **PÅ** (viktigt — WS-kanalen) |
|
||||
| Block Common Exploits | valfritt |
|
||||
|
||||
Under **SSL**: Request a new SSL Certificate (Let's Encrypt) + Force SSL + HTTP/2.
|
||||
|
||||
Spara. Öppna `https://rcai.brasse-pc.eu` → logga in med `AGENT_HELM_TOKEN`.
|
||||
|
||||
---
|
||||
|
||||
## Steg 5 — Anslut en daemon (annars finns inga sessioner)
|
||||
|
||||
Servern är bara control-plane. En **daemon** kör agenten och ansluter till servern.
|
||||
Kör den var du vill (workstation, eller som egen container senare):
|
||||
|
||||
```bash
|
||||
# på workstationen, i agent-helm-repot
|
||||
AGENT_HELM_TOKEN=<samma-token> \
|
||||
SERVER_URL=wss://rcai.brasse-pc.eu \
|
||||
AGENT_CMD=gemini \
|
||||
pnpm dev:daemon
|
||||
```
|
||||
|
||||
Daemonen dyker upp som en session i web-UI:t. (Kräver att NPM:s Websockets är på.)
|
||||
|
||||
---
|
||||
|
||||
## Uppdatera till senaste
|
||||
|
||||
1. `git push master` → runnern bygger + pushar `:latest`.
|
||||
2. Dockge → stacken `agent-helm` → **Update** (kör `docker compose pull && up -d`).
|
||||
|
||||
Det var det — senaste image:n körs.
|
||||
|
||||
---
|
||||
|
||||
## Felsökning
|
||||
|
||||
| Symptom | Trolig orsak / åtgärd |
|
||||
|---|---|
|
||||
| Runnern syns inte i Gitea | Fel/förbrukad token. Skapa ny, uppdatera env, redeploy `act-runner`. |
|
||||
| Workflow fastnar på `actions/checkout` | Runnern når inte github för att hämta action:en, eller inte `gitea-d:3000`. Kolla att runnern är på `srv_default`. |
|
||||
| `docker: command not found` i jobbet | Fel job-image. Labeln ska peka på `catthehacker/ubuntu:act-latest`. |
|
||||
| `push` nekas / `http: server gave HTTP response to HTTPS client` | Pusha till `localhost:5000` (inte `192.168.0.19:5000`) från Pi-runnern — localhost trustas som insecure automatiskt. |
|
||||
| `rcai` ger 502 | `agent-helm`-containern nere, eller fel forward-namn/port i NPM. |
|
||||
| WS ansluter inte (UI loggar in men sessioner laddar ej) | Websockets Support av i NPM-hosten. Slå på. |
|
||||
| Bygget kompilerar node-pty och failar | Ska inte hända — Dockerfilen filtrerar bort `daemon`. Om det sker: kontrollera `--filter`-raderna. |
|
||||
28
deploy/act-runner.compose.yaml
Normal file
28
deploy/act-runner.compose.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# Gitea Actions-runner (Docker) för Pi5 — bygger arm64-images nativt.
|
||||
#
|
||||
# Engångsregistrering: hämta en registrerings-token i Gitea
|
||||
# Site Administration -> Actions -> Runners -> "Create new runner"
|
||||
# och lägg den i en .env bredvid denna fil (Dockge: stackens env-flik):
|
||||
# GITEA_RUNNER_REGISTRATION_TOKEN=xxxxxxxx
|
||||
#
|
||||
# Lägg denna fil + config.yaml i /srv/dockge-staks/act-runner/ och starta i Dockge.
|
||||
services:
|
||||
act-runner:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: gitea-act-runner
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- srv_default
|
||||
environment:
|
||||
GITEA_INSTANCE_URL: http://gitea-d:3000 # intern adress på srv_default
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||
GITEA_RUNNER_NAME: pi5-docker-runner
|
||||
CONFIG_FILE: /config.yaml
|
||||
volumes:
|
||||
- /srv/docker/act-runner:/data # runner-state (persistent)
|
||||
- /var/run/docker.sock:/var/run/docker.sock # bygg via värdens Docker
|
||||
- ./config.yaml:/config.yaml:ro
|
||||
|
||||
networks:
|
||||
srv_default:
|
||||
external: true
|
||||
20
deploy/act-runner.config.yaml
Normal file
20
deploy/act-runner.config.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# act_runner-config. Nyckeln: job-containrar får värdens docker.sock så att
|
||||
# `docker build` / `docker push` i workflowen kör mot Pi5:ans Docker-daemon,
|
||||
# och localhost:5000 (registryn) trustas automatiskt.
|
||||
log:
|
||||
level: info
|
||||
|
||||
runner:
|
||||
capacity: 1
|
||||
# Label -> image som jobben körs i. catthehacker-imagen har node + docker-CLI.
|
||||
labels:
|
||||
- "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
|
||||
- "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
|
||||
|
||||
container:
|
||||
# Montera värdens docker.sock i varje job-container (för docker build/push).
|
||||
options: "-v /var/run/docker.sock:/var/run/docker.sock"
|
||||
valid_volumes:
|
||||
- /var/run/docker.sock
|
||||
# Job-containrar på srv_default så checkout når gitea-d:3000 internt.
|
||||
network: srv_default
|
||||
33
deploy/agent-helm.compose.yaml
Normal file
33
deploy/agent-helm.compose.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
# agent-helm control-plane + web-UI.
|
||||
# Image byggs av Gitea Actions och hämtas från den lokala registryn.
|
||||
# Uppdatera till senaste: Dockge -> denna stack -> "Update" (pull + recreate).
|
||||
#
|
||||
# Lägg i /srv/dockge-staks/agent-helm/ och starta i Dockge.
|
||||
# Sätt en stark token i stackens .env (Dockge: env-flik):
|
||||
# AGENT_HELM_TOKEN=<lång-slumpad-hemlighet>
|
||||
x-dockge:
|
||||
urls:
|
||||
- https://rcai.brasse-pc.eu
|
||||
|
||||
services:
|
||||
agent-helm:
|
||||
image: localhost:5000/agent-helm-server:latest
|
||||
container_name: agent-helm
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "8787" # NPM binder publikt; ingen host-port behövs
|
||||
environment:
|
||||
AGENT_HELM_TOKEN: ${AGENT_HELM_TOKEN}
|
||||
PORT: "8787"
|
||||
DATA_DIR: /app/data
|
||||
volumes:
|
||||
# Host-mountad config/data enligt övriga appars konvention (/srv/docker/<stack>).
|
||||
# Containern kör som uid 1000 (node) = brasse på host, så filer blir brasse-ägda.
|
||||
# Skapa katalogen först: mkdir -p /srv/docker/agent-helm
|
||||
- /srv/docker/agent-helm:/app/data
|
||||
networks:
|
||||
- srv_default # så NPM kan nå containern på namn
|
||||
|
||||
networks:
|
||||
srv_default:
|
||||
external: true
|
||||
@@ -7,6 +7,7 @@
|
||||
"dev": "tsx --env-file-if-exists=../../.env watch src/index.ts",
|
||||
"start": "tsx --env-file-if-exists=../../.env src/index.ts",
|
||||
"build": "tsc --noEmit -p tsconfig.json",
|
||||
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node22 --outfile=dist/server.cjs",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -18,6 +19,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/ws": "^8.5.12",
|
||||
"esbuild": "^0.25.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
* - Routing: web:input -> rätt daemons pty. daemon:output -> alla prenumeranter.
|
||||
*/
|
||||
import http from "node:http";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
// @xterm/headless och addon-serialize är CommonJS — default-importera värdet och
|
||||
// destrukturera; ta typen via `import type` (raderas i runtime).
|
||||
@@ -37,6 +40,11 @@ if (!TOKEN) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Persistent katalog (host-mountad på Pi5: /srv/docker/agent-helm -> /app/data).
|
||||
// Här hamnar framtida db/auth-state. Skapas vid start om den är satt.
|
||||
const DATA_DIR = process.env.DATA_DIR ?? "";
|
||||
if (DATA_DIR) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
interface Session {
|
||||
meta: SessionMeta;
|
||||
daemon: WebSocket | null;
|
||||
@@ -180,12 +188,58 @@ function handleClose(ws: WebSocket): void {
|
||||
}
|
||||
}
|
||||
|
||||
const WEB_DIST = process.env.WEB_DIST ?? "";
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".png": "image/png",
|
||||
".webmanifest": "application/manifest+json",
|
||||
".woff2": "font/woff2",
|
||||
".map": "application/json",
|
||||
};
|
||||
|
||||
/** Serverar den byggda web-frontenden (om WEB_DIST är satt) med SPA-fallback. */
|
||||
function serveStatic(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (!WEB_DIST) return false;
|
||||
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
||||
const indexHtml = path.join(WEB_DIST, "index.html");
|
||||
let filePath = path.normalize(path.join(WEB_DIST, urlPath));
|
||||
// Path-traversal-skydd + SPA: "/" och rutter utan filändelse -> index.html
|
||||
if (!filePath.startsWith(WEB_DIST) || urlPath === "/" || !path.extname(filePath)) {
|
||||
filePath = indexHtml;
|
||||
}
|
||||
fs.readFile(filePath, (err, buf) => {
|
||||
if (err) {
|
||||
fs.readFile(indexHtml, (err2, idx) => {
|
||||
if (err2) {
|
||||
res.writeHead(404);
|
||||
res.end("not found");
|
||||
} else {
|
||||
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
res.end(idx);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "content-type": MIME[path.extname(filePath)] ?? "application/octet-stream" });
|
||||
res.end(buf);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === "/health") {
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, sessions: sessions.size }));
|
||||
return;
|
||||
}
|
||||
if (serveStatic(req, res)) return;
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
@@ -208,4 +262,6 @@ wss.on("connection", (ws) => {
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[server] lyssnar på :${PORT} (WebSocket + /health)`);
|
||||
if (WEB_DIST) console.log(`[server] serverar web-UI från ${WEB_DIST}`);
|
||||
if (DATA_DIR) console.log(`[server] data-dir: ${DATA_DIR}`);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,11 @@ import { ref, computed } from "vue";
|
||||
import { createHelmClient } from "./lib/helm";
|
||||
import TerminalView from "./components/TerminalView.vue";
|
||||
|
||||
const defaultUrl = import.meta.env.VITE_SERVER_WS ?? "ws://localhost:8787";
|
||||
// I dev: peka mot lokal server. I prod (serverad av servern bakom NPM): samma
|
||||
// origin, så bygget funkar på vilken host som helst (t.ex. wss://rcai.brasse-pc.eu).
|
||||
const sameOriginWs = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}`;
|
||||
const defaultUrl =
|
||||
import.meta.env.VITE_SERVER_WS || (import.meta.env.DEV ? "ws://localhost:8787" : sameOriginWs);
|
||||
const defaultToken = import.meta.env.VITE_AGENT_HELM_TOKEN ?? "";
|
||||
|
||||
const url = ref(localStorage.getItem("helm.url") ?? defaultUrl);
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -58,6 +58,9 @@ importers:
|
||||
'@types/ws':
|
||||
specifier: ^8.5.12
|
||||
version: 8.18.1
|
||||
esbuild:
|
||||
specifier: ^0.25.0
|
||||
version: 0.25.12
|
||||
tsx:
|
||||
specifier: ^4.19.0
|
||||
version: 4.22.4
|
||||
|
||||
Reference in New Issue
Block a user