From 23d0c29e0bfc691b72f4f0afae3f79157d62cbd4 Mon Sep 17 00:00:00 2001 From: agent Date: Sat, 26 Sep 2026 22:33:08 +0000 Subject: [PATCH] Add Dockerfile and compose setup for self-hosting Multi-stage build: Node builds the static PWA, nginx serves it with an SPA fallback, long-lived caching for hashed assets, no-cache for the app shell and service worker, and the correct type for the web manifest. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VsorPV3JeJRoZ1mD1pdvtL --- .dockerignore | 9 +++++++++ Dockerfile | 14 ++++++++++++++ README.md | 8 ++++++++ compose.yaml | 8 ++++++++ docker/nginx.conf | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yaml create mode 100644 docker/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..12e029c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.claude +node_modules +.svelte-kit +build +memos +Dockerfile +compose.yaml +*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0f208b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Build the static PWA, then serve it with nginx. + +FROM node:24-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.29-alpine +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/build /usr/share/nginx/html +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO /dev/null http://127.0.0.1/ || exit 1 diff --git a/README.md b/README.md index 2ce1d3a..824eec3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ npm run check # type-check npm run build # static PWA in build/ — serve it from any static host (HTTPS for PWA install) ``` +## Docker + +```sh +docker compose up -d --build # http://localhost:8080 (set PAPURE_PORT to change) +``` + +The image builds the PWA and serves it with nginx. Put an HTTPS reverse proxy in front of it: installing the PWA and the offline service worker both need HTTPS. To update, `git pull` and run the same command again. + ## Using it - **Tools** (bottom bar): Text `T`, Pen `P`, Highlighter `H`, Eraser `E`, Import PDF, Undo/Redo, zoom. The pen's eraser end erases too. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..30d5a45 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + papure: + build: . + image: papure + restart: unless-stopped + # Put an HTTPS reverse proxy in front: the PWA install and service worker need HTTPS. + ports: + - "${PAPURE_PORT:-8080}:80" diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..f86d149 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + + gzip on; + gzip_types text/css application/javascript application/json application/manifest+json image/svg+xml; + + # Hashed build output never changes. + location /_app/immutable/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + # The service worker and app shell must be re-checked so updates reach clients. + location = /service-worker.js { + add_header Cache-Control "no-cache"; + try_files $uri =404; + } + + # nginx's default MIME table has no entry for the PWA manifest. + location ~ \.webmanifest$ { + types { } + default_type application/manifest+json; + add_header Cache-Control "no-cache"; + } + + # Client-side app: unknown paths fall back to the SPA shell. + location / { + add_header Cache-Control "no-cache"; + try_files $uri $uri/ /index.html; + } +}