Skip to content

feat: optional self-hosted UUID server (SQLite + shared viewer)#16

Open
baanish wants to merge 2 commits intomainfrom
cursor/agent-render-self-hosted-mode-2505
Open

feat: optional self-hosted UUID server (SQLite + shared viewer)#16
baanish wants to merge 2 commits intomainfrom
cursor/agent-render-self-hosted-mode-2505

Conversation

@baanish
Copy link
Owner

@baanish baanish commented Mar 20, 2026

Summary

Adds an optional deployment mode: a small Node server that stores the canonical agent-render=v1... payload string in SQLite under UUID v4 keys, with a 24h sliding TTL on successful GET /api/artifacts/:id. The same static-export viewer bundle decodes and renders artifacts when built with NEXT_PUBLIC_SELFHOSTED_SERVER=1.

The default fragment-based static product is unchanged when that env var is unset (including CI / Playwright).

Key changes

  • selfhosted/server.mjs (static out/, CRUD API, GET /{uuid}index.html), artifact-db.mjs (WAL SQLite, TTL refresh, lazy expiry on read), cleanup.mjs, Docker assets.
  • src/lib/payload/fragment.tsDecodeFragmentOptions / enforceFragmentLengthLimit so stored wires can exceed MAX_FRAGMENT_LENGTH while keeping decoded size limits.
  • src/components/viewer-shell.tsx — UUID path detection + API fetch + local activeArtifactId updates without rewriting the full payload to the hash; loading/error panels; data-transport attribute.
  • src/lib/selfhosted/artifact-path.ts — UUID segment parsing (works with Next usePathname() + optional base path).
  • src/components/viewer/fragment-details-disclosure.tsxtransportMode / expiry label for stored payloads.
  • Docs + skillsREADME.md, AGENTS.md, docs/deployment.md, docs/architecture.md, docs/payload-format.md, docs/testing.md, docs/dependency-notes.md, skills/agent-render-linking/SKILL.md, new skills/selfhosted-agent-render/SKILL.md.
  • Teststests/selfhosted-artifact-db.test.ts, tests/artifact-path.test.ts, fragment test for stored decode; dependency better-sqlite3.

Build / run (self-hosted)

NEXT_PUBLIC_SELFHOSTED_SERVER=1 npm run build
npm run selfhosted:start

API

  • POST /api/artifacts { "payload": "agent-render=v1...." }
  • GET|PUT|DELETE /api/artifacts/:id (GET extends TTL)

NEXT_PUBLIC_BASE_PATH is honored for static assets and API routes, consistent with the static preview server.

Testing

  • npm run check — pass
  • npm run test:e2e — pass (one run showed a single flaky JSON test under parallel load; npx playwright test tests/e2e/viewer.spec.ts:146 --repeat-each=3 — pass)
Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional self-hosted deployment mode with UUID-based artifact links backed by SQLite storage
    • 24-hour sliding TTL for stored artifacts; expired items are automatically cleaned up
    • Docker Compose support for containerized deployment
    • REST API for artifact management (create, read, update, delete operations)
    • Build-time flag to enable self-hosted server mode
  • Documentation

    • Updated deployment guides to distinguish static (default) from self-hosted modes
    • Added architecture and configuration documentation for self-hosted setup
    • Clarified privacy and data retention behavior per deployment mode

- Add Node selfhosted server (CRUD API, sliding 24h TTL, static + /{uuid} shell)
- Reuse viewer: fetch payload, decode with optional fragment-length bypass
- Gate UUID client behavior on NEXT_PUBLIC_SELFHOSTED_SERVER=1 for static safety
- Document deployment, add selfhosted-agent-render skill, tests for store and path

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces an optional self-hosted Node + SQLite deployment mode alongside the existing static fragment-based architecture. The changes add a separate server under selfhosted/ that persists canonical artifact payloads via UUID links with a sliding 24-hour TTL, Docker containerization support, and corresponding client-side integration to fetch and decode stored payloads. All default static behavior remains unchanged.

Changes

Cohort / File(s) Summary
Repository Configuration
.gitignore, package.json
Added /data/ directory to .gitignore, plus two new npm scripts (selfhosted:start, selfhosted:cleanup) and better-sqlite3 dependency.
Documentation & Skills
AGENTS.md, README.md, docs/architecture.md, docs/deployment.md, docs/payload-format.md, docs/testing.md, docs/dependency-notes.md, skills/agent-render-linking/SKILL.md, skills/selfhosted-agent-render/SKILL.md
Documented optional self-hosted mode across deployment guides, architecture notes, and agent workflow docs. Clarified that default static behavior (fragment-only, zero-retention) is primary, with optional separate Node server as add-on. Added build flag (NEXT_PUBLIC_SELFHOSTED_SERVER=1), API contracts, TTL/cleanup mechanics, and Docker deployment patterns.
Self-hosted Server Implementation
selfhosted/README.md, selfhosted/server.mjs, selfhosted/artifact-db.mjs, selfhosted/cleanup.mjs, selfhosted/Dockerfile, selfhosted/docker-compose.yml
New self-hosted server stack: HTTP API server handling artifact CRUD (POST/GET/PUT/DELETE /api/artifacts/{id}), SQLite-backed storage with sliding TTL and lazy expiry deletion, batch cleanup script, and Docker containerization with persistent volume.
Client-side Self-hosted Support
src/components/viewer-shell.tsx, src/components/viewer/fragment-details-disclosure.tsx, src/lib/selfhosted/artifact-path.ts, src/lib/payload/fragment.ts
Extended ViewerShell to detect and fetch UUID artifacts from basePath/api/artifacts/{id} when NEXT_PUBLIC_SELFHOSTED_SERVER=1, managing separate state for stored payloads. Added UUID path extraction utility and optional fragment-length enforcement bypass for stored payloads. Updated downstream components to render transport mode and expiry info conditionally.
Tests
tests/artifact-path.test.ts, tests/fragment.test.ts, tests/selfhosted-artifact-db.test.ts
Added test coverage for UUID path parsing, fragment decoding with optional length enforcement bypass, and full artifact database lifecycle (create/read/update/delete with expiry and TTL refresh).

Sequence Diagram

sequenceDiagram
    participant Client as Client (Browser)
    participant Router as Route Handler
    participant API as Server API
    participant DB as SQLite DB
    participant Static as Static Files

    Client->>Router: GET /artifact-uuid
    activate Router
    Router->>Router: Extract UUID from pathname
    Router->>API: GET /api/artifacts/{uuid}
    deactivate Router
    
    activate API
    API->>DB: SELECT artifact WHERE id=uuid
    DB-->>API: artifact row (if exists & not expired)
    
    alt Artifact expired
        API->>DB: DELETE artifact
        DB-->>API: deleted
        API-->>Client: 404 {error: "expired"}
    else Artifact valid
        API->>DB: UPDATE expires_at (sliding TTL)
        DB-->>API: updated
        API-->>Client: 200 {payload, expiresAt}
    end
    deactivate API
    
    activate Client
    Client->>Client: Decode payload<br/>(enforceFragmentLengthLimit=false)
    Client->>Static: GET /index.html (cached)
    Static-->>Client: HTML shell
    Client->>Client: Render viewer with stored payload
    deactivate Client
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #14: Modifies src/lib/payload/fragment.ts to add new ARX base64url candidate and transport-length computation, overlapping with this PR's fragment decoding option threading.
  • PR #6: Updates src/components/viewer-shell.tsx home-navigation logic (handleGoHome), related to self-hosted viewer navigation changes in this PR.
  • PR #13: Modifies src/components/viewer-shell.tsx UI state for artifact copy/clipboard toolbar, overlapping with this PR's new internal state management for stored artifacts.

Poem

🐰 Hops with glee, a new path unfurls,
SQLite whispers to UUID pearls,
Self-hosted dreams on a sliding TTL,
Twenty-four hours of artifact thrill,
Fragment or stored—both journeys are real! 📦✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely describes the main feature: adding an optional self-hosted UUID server with SQLite and a shared viewer component, which matches the primary objective and the majority of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/agent-render-self-hosted-mode-2505

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 20, 2026

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 38d1cc5
Status: ✅  Deploy successful!
Preview URL: https://33df3592.agent-render.pages.dev
Branch Preview URL: https://cursor-agent-render-self-hos.agent-render.pages.dev

View logs

@kilo-code-bot
Copy link

kilo-code-bot bot commented Mar 20, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (24 files)
  • .gitignore - Added /data/ for SQLite
  • AGENTS.md - Updated docs for self-hosted mode
  • README.md - Added self-hosted documentation
  • docs/architecture.md - Updated for optional server mode
  • docs/dependency-notes.md - Added better-sqlite3 MIT dependency
  • docs/deployment.md - Full self-hosted deployment guide
  • docs/payload-format.md - Documented stored-mode decode
  • docs/testing.md - Added self-hosted test coverage
  • package.json - Added selfhosted scripts and better-sqlite3
  • selfhosted/server.mjs - HTTP server with CRUD API
  • selfhosted/artifact-db.mjs - SQLite with 24h TTL
  • selfhosted/cleanup.mjs - Expiry purge script
  • selfhosted/Dockerfile - Container definition
  • selfhosted/docker-compose.yml - Docker Compose
  • src/components/viewer-shell.tsx - UUID fetch path
  • src/components/viewer/fragment-details-disclosure.tsx - Transport mode UI
  • src/lib/payload/fragment.ts - enforceFragmentLengthLimit option
  • src/lib/selfhosted/artifact-path.ts - UUID parsing
  • tests/fragment.test.ts - Stored-mode decode tests
  • tests/artifact-path.test.ts - UUID path tests
  • tests/selfhosted-artifact-db.test.ts - SQLite tests
  • skills/selfhosted-agent-render/SKILL.md - New skill
  • skills/agent-render-linking/SKILL.md - Updated

The PR adds an optional self-hosted deployment mode with SQLite storage, properly documented as an opt-in feature that doesn't affect the default static fragment product.


Reviewed by minimax-m2.5-20260211 · 1,134,843 tokens

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36c476cff1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +269 to +271
server.listen(port, () => {
const suffix = basePath ? `${basePath}/` : "/";
console.log(`Self-hosted agent-render at http://127.0.0.1:${port}${suffix}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the self-hosted server to loopback by default

server.listen(port) omits the host, and Node will accept connections on ::/0.0.0.0 when the hostname is missing. In practice, anyone who runs npm run selfhosted:start on a machine with a routable interface exposes the UUID artifact API to the network, even though the startup log advertises 127.0.0.1 and the docs describe same-machine usage. Defaulting to loopback (or requiring an explicit HOST) would avoid accidental public exposure of stored payloads.

Useful? React with 👍 / 👎.

Comment on lines +68 to +71
function toStaticFilePath(requestPath) {
const normalizedPath = requestPath === "/" ? "/index.html" : requestPath;
const tentativePath = path.join(staticRoot, normalizedPath);
return normalizedPath.endsWith("/") ? path.join(tentativePath, "index.html") : tentativePath;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject path traversal before serving static files

The static-file fallback joins the raw request path straight onto staticRoot without checking for .. segments, so a client that preserves the path verbatim (for example, curl --path-as-is /../../data/artifacts.sqlite) can escape the export directory. Because the resulting path is then passed through existsSync and createReadStream, a reachable self-hosted instance can disclose arbitrary files from the host filesystem, including the SQLite database itself.

Useful? React with 👍 / 👎.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
selfhosted/docker-compose.yml (1)

1-17: Consider adding restart policy and healthcheck for production resilience.

The compose file is functionally correct. For production deployments, consider adding a restart policy and healthcheck to improve reliability.

♻️ Optional production hardening
 services:
   agent-render-selfhosted:
     build:
       context: ..
       dockerfile: selfhosted/Dockerfile
       args:
         NEXT_PUBLIC_SELFHOSTED_SERVER: "1"
     ports:
       - "3000:3000"
     environment:
       PORT: "3000"
       DATABASE_PATH: /data/artifacts.sqlite
     volumes:
       - agent-render-data:/data
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "node", "-e", "fetch('http://localhost:3000/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
+      interval: 30s
+      timeout: 10s
+      retries: 3

 volumes:
   agent-render-data:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@selfhosted/docker-compose.yml` around lines 1 - 17, Add a restart policy and
a healthcheck to the agent-render-selfhosted service to improve production
resilience: under the service definition for agent-render-selfhosted add a
restart (e.g., "unless-stopped" or "always") and a healthcheck block that
exercises the running app (e.g., HTTP GET to localhost:3000/ or a lightweight
curl command) with sensible interval, timeout and retries so Docker can detect
and restart unhealthy containers; ensure the healthcheck references the
container port and leaves the existing volumes (agent-render-data) and
environment intact.
tests/artifact-path.test.ts (1)

6-19: Consider additional edge case coverage.

The current tests cover the happy path well. Consider adding tests for:

  • Invalid UUID format (e.g., /not-a-uuid/) → should return null
  • Nested paths beyond the UUID (e.g., /app/${sample}/extra) → clarify expected behavior
  • Empty string basePath vs undefined
💡 Suggested additional test cases
it("returns null for invalid UUID format", () => {
  expect(getArtifactIdFromPathname("/not-a-uuid/")).toBeNull();
  expect(getArtifactIdFromPathname("/123/")).toBeNull();
});

it("handles empty basePath same as undefined", () => {
  expect(getArtifactIdFromPathname(`/${sample}/`, "")).toBe(sample);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/artifact-path.test.ts` around lines 6 - 19, Add unit tests for edge
cases to getArtifactIdFromPathname: verify invalid UUID segments return null
(e.g., expect getArtifactIdFromPathname("/not-a-uuid/") and
getArtifactIdFromPathname("/123/") toBeNull), assert nested extra segments after
the UUID return null (e.g., expect
getArtifactIdFromPathname(`/app/${sample}/extra`, "/app") toBeNull) to enforce
exact-segment matching, and add a test that an empty string basePath is treated
like undefined (e.g., expect getArtifactIdFromPathname(`/${sample}/`, "")
toBe(sample)); reference the existing test suite around
getArtifactIdFromPathname to add these cases.
selfhosted/server.mjs (1)

79-93: Consider adding request timeout handling.

The readBody function limits body size to 12MB but doesn't have a timeout for slow clients. A malicious or slow client could hold the connection open indefinitely by sending data very slowly.

♻️ Optional: Add request timeout
 function readBody(request) {
   return new Promise((resolve, reject) => {
+    const timeout = setTimeout(() => {
+      reject(new Error("request timeout"));
+    }, 30000);
+
     const chunks = [];
     request.on("data", (chunk) => {
       chunks.push(chunk);
       if (chunks.reduce((acc, c) => acc + c.length, 0) > 12_000_000) {
+        clearTimeout(timeout);
         reject(new Error("body too large"));
       }
     });
     request.on("end", () => {
+      clearTimeout(timeout);
       resolve(Buffer.concat(chunks).toString("utf8"));
     });
-    request.on("error", reject);
+    request.on("error", (err) => {
+      clearTimeout(timeout);
+      reject(err);
+    });
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@selfhosted/server.mjs` around lines 79 - 93, The readBody function lacks a
timeout, allowing slow or malicious clients to keep the connection open; add
timeout handling by starting a timer when readBody begins (or use
request.setTimeout) that rejects with a descriptive Error (e.g., "request
timeout") if no completion before the deadline, and ensure you clear the timer
and remove/cleanup listeners on "end", "error", and on timeout; update the
Promise handlers in readBody to call request.destroy() or similar after
rejecting to free resources and avoid memory leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@selfhosted/artifact-db.mjs`:
- Around line 55-59: The touchView prepared statement (touchView) is incorrectly
updating updated_at on reads; modify the touchView SQL so it only updates
last_viewed_at and expires_at (leave updated_at untouched) when refreshing TTL
on a GET, and ensure any legitimate updates to updated_at remain confined to the
write/update path (e.g., the function that handles PUT/modify of artifacts).

In `@selfhosted/Dockerfile`:
- Around line 1-24: Add a non-root user and switch to it after installing
dependencies/building the app: create a user/group (e.g., appuser), chown the
application directories and the mounted VOLUME path so the user can write to
/data and STATIC_ROOT, then set USER to that non-root user before the final CMD
(which currently runs node selfhosted/server.mjs); perform these steps after RUN
npm ci and RUN npm run build so build steps run as root but runtime runs
unprivileged, and ensure any runtime environment variables (e.g., DATABASE_PATH,
STATIC_ROOT) remain accessible to the new user.

---

Nitpick comments:
In `@selfhosted/docker-compose.yml`:
- Around line 1-17: Add a restart policy and a healthcheck to the
agent-render-selfhosted service to improve production resilience: under the
service definition for agent-render-selfhosted add a restart (e.g.,
"unless-stopped" or "always") and a healthcheck block that exercises the running
app (e.g., HTTP GET to localhost:3000/ or a lightweight curl command) with
sensible interval, timeout and retries so Docker can detect and restart
unhealthy containers; ensure the healthcheck references the container port and
leaves the existing volumes (agent-render-data) and environment intact.

In `@selfhosted/server.mjs`:
- Around line 79-93: The readBody function lacks a timeout, allowing slow or
malicious clients to keep the connection open; add timeout handling by starting
a timer when readBody begins (or use request.setTimeout) that rejects with a
descriptive Error (e.g., "request timeout") if no completion before the
deadline, and ensure you clear the timer and remove/cleanup listeners on "end",
"error", and on timeout; update the Promise handlers in readBody to call
request.destroy() or similar after rejecting to free resources and avoid memory
leaks.

In `@tests/artifact-path.test.ts`:
- Around line 6-19: Add unit tests for edge cases to getArtifactIdFromPathname:
verify invalid UUID segments return null (e.g., expect
getArtifactIdFromPathname("/not-a-uuid/") and getArtifactIdFromPathname("/123/")
toBeNull), assert nested extra segments after the UUID return null (e.g., expect
getArtifactIdFromPathname(`/app/${sample}/extra`, "/app") toBeNull) to enforce
exact-segment matching, and add a test that an empty string basePath is treated
like undefined (e.g., expect getArtifactIdFromPathname(`/${sample}/`, "")
toBe(sample)); reference the existing test suite around
getArtifactIdFromPathname to add these cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 277fd8c9-8e28-4127-a9ab-8b434e37587f

📥 Commits

Reviewing files that changed from the base of the PR and between 3d5ed45 and 38d1cc5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (24)
  • .gitignore
  • AGENTS.md
  • README.md
  • docs/architecture.md
  • docs/dependency-notes.md
  • docs/deployment.md
  • docs/payload-format.md
  • docs/testing.md
  • package.json
  • selfhosted/Dockerfile
  • selfhosted/README.md
  • selfhosted/artifact-db.mjs
  • selfhosted/cleanup.mjs
  • selfhosted/docker-compose.yml
  • selfhosted/server.mjs
  • skills/agent-render-linking/SKILL.md
  • skills/selfhosted-agent-render/SKILL.md
  • src/components/viewer-shell.tsx
  • src/components/viewer/fragment-details-disclosure.tsx
  • src/lib/payload/fragment.ts
  • src/lib/selfhosted/artifact-path.ts
  • tests/artifact-path.test.ts
  • tests/fragment.test.ts
  • tests/selfhosted-artifact-db.test.ts

Comment on lines +55 to +59
const touchView = db.prepare(`
UPDATE artifacts
SET last_viewed_at = @now, expires_at = @expires_at, updated_at = @now
WHERE id = @id
`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

updated_at is modified on GET, which may not be intended.

The touchView statement sets updated_at = @now`` when refreshing the TTL on a read operation. Typically, updated_at should only change when the payload is modified (PUT), while `last_viewed_at` tracks read access. This could confuse API consumers who expect `updatedAt` to reflect actual content changes.

🔧 Proposed fix to preserve `updated_at` on view
   const touchView = db.prepare(`
     UPDATE artifacts
-    SET last_viewed_at = `@now`, expires_at = `@expires_at`, updated_at = `@now`
+    SET last_viewed_at = `@now`, expires_at = `@expires_at`
     WHERE id = `@id`
   `);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@selfhosted/artifact-db.mjs` around lines 55 - 59, The touchView prepared
statement (touchView) is incorrectly updating updated_at on reads; modify the
touchView SQL so it only updates last_viewed_at and expires_at (leave updated_at
untouched) when refreshing TTL on a GET, and ensure any legitimate updates to
updated_at remain confined to the write/update path (e.g., the function that
handles PUT/modify of artifacts).

Comment on lines +1 to +24
FROM node:22-bookworm-slim

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci

ARG NEXT_PUBLIC_BASE_PATH=
ARG NEXT_PUBLIC_SELFHOSTED_SERVER=1
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
ENV NEXT_PUBLIC_SELFHOSTED_SERVER=$NEXT_PUBLIC_SELFHOSTED_SERVER

COPY . .
RUN npm run build

ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_PATH=/data/artifacts.sqlite
ENV STATIC_ROOT=/app/out

VOLUME ["/data"]
EXPOSE 3000

CMD ["node", "selfhosted/server.mjs"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a non-root user for container security.

The container runs as root by default, which increases the attack surface if the application is compromised. Consider adding a non-root user after installing dependencies and before the CMD.

🛡️ Proposed fix to run as non-root user
 ENV STATIC_ROOT=/app/out

+RUN addgroup --system --gid 1001 nodejs && \
+    adduser --system --uid 1001 --ingroup nodejs appuser && \
+    chown -R appuser:nodejs /app
+
 VOLUME ["/data"]
 EXPOSE 3000

+USER appuser
+
 CMD ["node", "selfhosted/server.mjs"]

Note: Ensure the /data volume is writable by the non-root user at runtime (e.g., via chown in an entrypoint or by mounting with appropriate permissions).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
ARG NEXT_PUBLIC_BASE_PATH=
ARG NEXT_PUBLIC_SELFHOSTED_SERVER=1
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
ENV NEXT_PUBLIC_SELFHOSTED_SERVER=$NEXT_PUBLIC_SELFHOSTED_SERVER
COPY . .
RUN npm run build
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_PATH=/data/artifacts.sqlite
ENV STATIC_ROOT=/app/out
VOLUME ["/data"]
EXPOSE 3000
CMD ["node", "selfhosted/server.mjs"]
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
ARG NEXT_PUBLIC_BASE_PATH=
ARG NEXT_PUBLIC_SELFHOSTED_SERVER=1
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
ENV NEXT_PUBLIC_SELFHOSTED_SERVER=$NEXT_PUBLIC_SELFHOSTED_SERVER
COPY . .
RUN npm run build
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_PATH=/data/artifacts.sqlite
ENV STATIC_ROOT=/app/out
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 --ingroup nodejs appuser && \
chown -R appuser:nodejs /app
VOLUME ["/data"]
EXPOSE 3000
USER appuser
CMD ["node", "selfhosted/server.mjs"]
🧰 Tools
🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@selfhosted/Dockerfile` around lines 1 - 24, Add a non-root user and switch to
it after installing dependencies/building the app: create a user/group (e.g.,
appuser), chown the application directories and the mounted VOLUME path so the
user can write to /data and STATIC_ROOT, then set USER to that non-root user
before the final CMD (which currently runs node selfhosted/server.mjs); perform
these steps after RUN npm ci and RUN npm run build so build steps run as root
but runtime runs unprivileged, and ensure any runtime environment variables
(e.g., DATABASE_PATH, STATIC_ROOT) remain accessible to the new user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants