vinterliste/Containerfile
Ole-Morten Duesund 47963c9225 Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.

Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
  recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
  semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)

Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
  SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
  sources with clear labelling
- Bokmål UI

Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
  that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
  on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
  a visually-unambiguous alphabet

Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
  change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped

Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.

Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00

67 lines
2.3 KiB
Docker

# syntax=docker/dockerfile:1
#
# Vinterliste — single-image container.
# Build stage compiles the frontend with Vite; runtime image runs Bun.
ARG BUN_VERSION=1.3
ARG BUILD_DATE
ARG GIT_REVISION
# ---- Builder ---------------------------------------------------------------
FROM docker.io/oven/bun:${BUN_VERSION} AS builder
WORKDIR /app
# Install dependencies first so they cache independently of source.
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile || bun install
# Copy the rest of the source and build the frontend bundle.
COPY tsconfig.json ./
COPY shared ./shared
COPY server ./server
COPY frontend ./frontend
RUN bun run build:frontend
# ---- Runtime ---------------------------------------------------------------
FROM docker.io/oven/bun:${BUN_VERSION}-slim
WORKDIR /app
ARG BUILD_DATE
ARG GIT_REVISION
# OCI labels keep the image traceable even when tagged :latest.
LABEL org.opencontainers.image.title="vinterliste" \
org.opencontainers.image.description="End-to-end encrypted winter activity list" \
org.opencontainers.image.source="https://example.invalid/vinterliste" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${GIT_REVISION}"
# Bake the same info into /etc/build-info so a running container can report it.
RUN printf 'build_date=%s\ngit_revision=%s\n' "${BUILD_DATE:-unknown}" "${GIT_REVISION:-unknown}" > /etc/build-info
# Copy production artefacts only. node_modules is reproducible from package.json,
# but we install fresh to keep the runtime image small and unambiguous.
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile --production || bun install --production
COPY --from=builder /app/shared ./shared
COPY --from=builder /app/server ./server
COPY --from=builder /app/frontend/dist ./frontend/dist
# SQLite WAL files live in /app/data, which is the documented mount point.
RUN mkdir -p /app/data && chown -R bun:bun /app/data
VOLUME /app/data
ENV NODE_ENV=production \
PORT=3000 \
VINTERLISTE_DB=/app/data/vinterliste.db \
BUILD_DATE=${BUILD_DATE} \
GIT_REVISION=${GIT_REVISION}
EXPOSE 3000
USER bun
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD bun -e "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["bun", "run", "server/index.ts"]