Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Build-context hygiene. Both Dockerfile and Dockerfile.demo run `npm ci` and
# build fresh INSIDE the image; a later `COPY web/ ./` (or `COPY web/ ./web/`)
# must not overlay the host's platform-specific node_modules onto the
# container's install, nor drag stale build output into the context.
**/node_modules
**/dist
web/dev-dist

# VCS, scratch, and local-only tooling
.git
.gitignore
.superpowers
.playwright-mcp
**/*.log

# Never bake secrets into the build context (mounted at runtime, not built in)
.env
*.pem
16 changes: 16 additions & 0 deletions Dockerfile.demo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Standalone public demo (demo.diffsentry.app). Builds the SPA with demo mode
# hard-forced (VITE_FORCE_DEMO=true, served at root, no service worker), then
# serves the static bundle with nginx. No server, DB, keys, or .diffsentry.yaml.
FROM node:22-alpine AS build
WORKDIR /app
COPY web/package.json web/package-lock.json ./web/
RUN npm --prefix web ci
COPY web/ ./web/
ENV VITE_FORCE_DEMO=true
RUN npm --prefix web run build

FROM nginx:alpine
COPY deploy/demo-nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/web/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:80/health || exit 1
57 changes: 57 additions & 0 deletions deploy/demo-nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
server {
listen 80;
listen [::]:80;
server_name demo.diffsentry.app localhost;
root /usr/share/nginx/html;
index index.html;

add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# Built React app, fixtures only — no secrets, no auth, no user input, no API
# (demo makes zero network calls). The app's index.html ships one inline
# theme/FOUC bootstrap script; rather than open script-src to 'unsafe-inline'
# (which the sibling diffsentry.app nginx deliberately avoids), we pin its
# sha256 so only that exact script runs inline. If that bootstrap is ever
# edited, this hash must be recomputed — the mismatch is fail-loud (a visible
# CSP console error), so the demo smoke catches it. 'unsafe-inline' style
# covers injected component styles (CodeMirror, etc.). No 'unsafe-eval'.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-UDmC3IkULnzs9gwNDZm9m/uQ3xynmJVzhBrYq5MnW6Q='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; base-uri 'self'; frame-ancestors 'none'" always;

# The SPA shell (index.html) is the release manifest — it points at hashed
# /assets bundles that are replaced on every deploy. A stale cached shell would
# request bundles that no longer exist, i.e. broken loads. Default everything
# to no-store; the /assets location below overrides this with immutable for the
# content-addressed bundles. Set at server level (not a per-location add_header
# on the shell) so index.html keeps the security + CSP headers above — a
# location-level add_header would drop that inheritance.
add_header Cache-Control "no-store" always;

gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;

Comment thread
diffsentry[bot] marked this conversation as resolved.
# Hashed Vite assets are content-addressed and safe to cache hard.
location /assets/ {
expires 30d;
add_header Cache-Control "public, immutable" always;
# nginx replaces (does not merge) inherited add_headers when a location
# defines its own, so re-assert the security headers here — otherwise the
# hashed JS/CSS ship without them. CSP is intentionally omitted: a CSP
# *response* header has no effect on a static sub-resource (script/style/
# font) — only the enclosing document's CSP governs it — so there is nothing
# to gain by duplicating the long policy string here.
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
}

Comment thread
diffsentry[bot] marked this conversation as resolved.
# SPA client routing: unknown paths serve the app shell.
location / {
try_files $uri $uri/ /index.html;
}

Comment thread
diffsentry[bot] marked this conversation as resolved.
location = /health {
default_type text/plain;
return 200 'ok';
}
}
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,14 @@ services:
- diffsentry-data:/app/data
restart: unless-stopped

demo:
build:
context: .
dockerfile: Dockerfile.demo
ports:
- "3027:80"
restart: unless-stopped
container_name: diffsentry-demo

volumes:
diffsentry-data:
Loading
Loading