feat: Dockerfile + docker-compose for Node.js deployment (#271)

* feat: add Dockerfile and docker-compose for self-hosted Node.js deployment

Multi-stage build using node:20-slim. Builder runs pnpm build:pages and
the server bundle; runtime image carries the bundled entry, native deps
(better-sqlite3) and the static frontend. A symlink keeps entry-node.ts's
relative `../../dist` static root resolving correctly when running from
packages/server/dist. Compose mounts a named volume at /data so the
SQLite database persists across restarts.

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

* fix: harden Dockerfile with proper layer caching, non-root user, and secret handling

Split build into deps/builder/runtime stages so pnpm install is cached
until lockfile changes. Run the Node process as a non-root `zpan` user.
Replace hardcoded BETTER_AUTH_SECRET with a required env var that fails
loudly when missing.

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
This commit is contained in:
Jasper Van
2026-04-09 17:22:18 -04:00
committed by GitHub
parent e66c2e508c
commit efec832c6f
3 changed files with 92 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
**/node_modules
**/dist
**/.turbo
**/.cache
**/coverage
**/.wrangler
.git
.github
.vscode
.idea
e2e
playwright-report
test-results
*.log
.env
.env.*
!.env.example
Dockerfile
docker-compose.yml
.dockerignore
+55
View File
@@ -0,0 +1,55 @@
# syntax=docker/dockerfile:1.7
# ── Stage 1: install deps (cached until lockfile changes) ──────
FROM node:20-slim AS deps
RUN corepack enable && corepack prepare pnpm@10.30.2 --activate
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/server/package.json packages/server/
COPY packages/web/package.json packages/web/
COPY packages/shared/package.json packages/shared/
RUN pnpm install --frozen-lockfile
# ── Stage 2: build ─────────────────────────────────────────────
FROM deps AS builder
COPY packages/ packages/
RUN pnpm build:pages
RUN pnpm --filter @zpan/server build
# Extract production-only deps into a flat directory.
# pnpm deploy bundles workspace packages (e.g. @zpan/shared) into node_modules.
RUN pnpm --filter @zpan/server deploy --prod /app/pruned
# ── Stage 3: runtime ───────────────────────────────────────────
FROM node:20-slim
RUN addgroup --system zpan && adduser --system --ingroup zpan zpan
WORKDIR /app
# Frontend assets (vite outputs to repo-root/dist, see packages/web/vite.config.ts).
# Placed at /app/packages/dist so the server's `serveStatic({ root: '../../dist' })`
# resolves correctly when cwd is /app/packages/server/dist.
COPY --from=builder /app/dist ./packages/dist
# Bundled server entrypoint
COPY --from=builder /app/packages/server/dist ./packages/server/dist
# Production node_modules (native deps like better-sqlite3 + workspace deps).
# Node resolves /app/packages/server/node_modules from cwd /app/packages/server/dist.
COPY --from=builder /app/pruned/node_modules ./packages/server/node_modules
RUN mkdir -p /data && chown zpan:zpan /data
USER zpan
ENV NODE_ENV=production
ENV PORT=8222
EXPOSE 8222
WORKDIR /app/packages/server/dist
CMD ["node", "entry-node.js"]
+17
View File
@@ -0,0 +1,17 @@
services:
zpan:
build: .
image: zpan:latest
ports:
- "8222:8222"
environment:
- PORT=8222
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in .env or shell}
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:8222}
- DATABASE_URL=/data/zpan.db
volumes:
- zpan-data:/data
restart: unless-stopped
volumes:
zpan-data: