diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..14def60 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +.git +*.sqlite +*.sqlite-* +config.jsonc +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7bf6115 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM node:24-bookworm-slim + +# git is needed for the read-only /git/status and /git/diff endpoints +RUN apt-get update \ + && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY tsconfig.json config.example.jsonc ./ +COPY src ./src +COPY scripts ./scripts + +# Non-root user. Mounted vaults are usually owned by a host uid that doesn't +# match the container user, which git refuses to touch by default — allow it +# system-wide (read-only endpoints only; braindump never commits). +RUN useradd -m braindump \ + && git config --system --add safe.directory '*' \ + && mkdir -p /data /config /vaults \ + && chown braindump /data + +USER braindump +ENV BRAINDUMP_CONFIG=/config/config.jsonc \ + BRAINDUMP_DB=/data/braindump.sqlite \ + NODE_ENV=production + +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD node -e "fetch('http://localhost:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["npx", "tsx", "src/bin/braindump-web.ts"] diff --git a/README.md b/README.md index 0460084..122afdf 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,51 @@ Config is JSONC; path from `BRAINDUMP_CONFIG` (default `./config.jsonc`). `"${EN There is **no auth** — put it behind an authenticating reverse proxy. CORS origins are configurable (`app://obsidian.md` is in the example for Obsidian plugins). +## Deployment (Docker) + +```sh +docker build -t braindump . +``` + +The image expects three mounts (paths inside the container are up to you; these match the defaults baked into the image via `BRAINDUMP_CONFIG=/config/config.jsonc` and `BRAINDUMP_DB=/data/braindump.sqlite`): + +| Mount | Purpose | +|---|---| +| `/config/config.jsonc` | your config — point `vaults[].path` at the container-side vault paths | +| `/data` | persistent volume for the SQLite db | +| `/vaults/...` | your vault(s), read-write | + +Backend-specific requirements: + +- **`claude-sdk`**: pass `ANTHROPIC_API_KEY`, and mount the brain-dump skill into the container user's home so the SDK's skill loading finds it: `-v ~/.claude/skills/brain-dump:/home/braindump/.claude/skills/brain-dump:ro`. +- **`openai-compat`**: mount the skill file wherever `skillPath` in your config points (e.g. `/config/SKILL.md`), and pass any `${ENV_VAR}` referenced by the config (e.g. `OPENROUTER_API_KEY`). + +Example `compose.yaml`: + +```yaml +services: + braindump: + build: . # or image: gitea.codinget.me/codinget/braindump + ports: ["3000:3000"] + environment: + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + volumes: + - ./config.jsonc:/config/config.jsonc:ro + - braindump-data:/data + - ~/git/brain:/vaults/brain + - ~/.claude/skills/brain-dump:/home/braindump/.claude/skills/brain-dump:ro +volumes: + braindump-data: +``` + +with the corresponding config using container paths: `"vaults": [{ "name": "brain", "path": "/vaults/brain" }]` and `"skillPath": "/home/braindump/.claude/skills/brain-dump/SKILL.md"`. + +Notes: + +- The container runs as a non-root `braindump` user; git `safe.directory` is pre-configured system-wide so the read-only git endpoints work on vaults owned by your host uid. Vault *writes* need the mounted vault to be writable by the container user (`chmod`/`chown` or a matching uid via `user:`). +- The healthcheck and config default both use port 3000; if you change the port, change both (`PORT` env works too, and overrides the config). +- **Reverse proxy + SSE**: disable response buffering for `/api/sessions/*/messages` (nginx: `proxy_buffering off;`, or honor the `X-Accel-Buffering: no` pattern) and raise/disable the proxy read timeout for long turns — heartbeat comments are sent every ~15s to keep the connection alive. This is also where your auth goes. + ## API Base path `/api`. Errors are `{"ok":false,"error":""}`. diff --git a/src/config/config.ts b/src/config/config.ts index 3e4f0ab..413ac74 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -145,7 +145,9 @@ export function parseConfig(raw: unknown): Config { const port = obj.port === undefined ? 3000 : obj.port; if (typeof port !== "number") errors.push("port must be a number"); - const dbPath = obj.dbPath; + // Optional: BRAINDUMP_DB overrides it after parsing, and container images + // bake that env in — so a missing dbPath just falls back to the default. + const dbPath = obj.dbPath === undefined ? "./braindump.sqlite" : obj.dbPath; if (typeof dbPath !== "string" || !dbPath) errors.push("dbPath must be a non-empty string"); const cors = obj.cors;