chore(deploy): harden Docker/compose from the deployment audit
- Dockerfile: npm ci (reproducible, lock-pinned, matches CI) instead of npm install; add a HEALTHCHECK against the /api/health liveness route. - package.json: pin engines node>=22.18.0 — the server require()s .mts/.cts and relies on Node's default TS type-stripping (unflagged only on >=22.18), which was previously undocumented on a floating base image. - .dockerignore: exclude .env / .env.* so a stray env file can't be baked into the image by COPY . . - docker-compose: set TRUST_PROXY=true (behind the reverse proxy, so Secure cookies + client IP for rate-limit/audit are correct); flip CSRF_HTTP_ONLY to true (SPA reads the token from an endpoint, so no JS cookie access needed); remove the active default INIT_ADMIN_PASS; document TOKEN_ENCRYPTION_KEY (set it to move at-rest secrets off the DB-resident key); drop obsolete version key. Not included: TOKEN_ENCRYPTION_KEY itself — that's a secret to generate and inject into the deployment env (the app self-migrates to the env-key scheme on next boot); documented in compose + .env.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a03216ee55
commit
f3f46a73f0
|
|
@ -6,3 +6,7 @@ data/
|
||||||
*.log
|
*.log
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
# Never bake secrets into the image (COPY . . would otherwise include a stray .env)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
|
||||||
10
Dockerfile
10
Dockerfile
|
|
@ -5,9 +5,10 @@ WORKDIR /app
|
||||||
# native build deps (better-sqlite3 etc)
|
# native build deps (better-sqlite3 etc)
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
# install ALL deps (vite needs dev deps)
|
# install ALL deps (vite needs dev deps) — npm ci for a reproducible, lock-pinned
|
||||||
|
# build that matches CI (which also uses npm ci).
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm ci
|
||||||
|
|
||||||
# copy full project
|
# copy full project
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
@ -44,5 +45,10 @@ ENV NODE_ENV=production \
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
# Liveness probe against the unauthenticated /api/health route (uses node, which
|
||||||
|
# is always present — no curl/wget needed).
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
||||||
|
|
||||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
bill-tracker:
|
bill-tracker:
|
||||||
image: dream.scheller.ltd/null/billtracker:latest
|
image: dream.scheller.ltd/null/billtracker:latest
|
||||||
|
|
@ -9,11 +7,24 @@ services:
|
||||||
- "3030:3000"
|
- "3030:3000"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
INIT_ADMIN_USER: admin
|
# First-run admin bootstrap: set INIT_ADMIN_USER/INIT_ADMIN_PASS here ONLY
|
||||||
INIT_ADMIN_PASS: changeme123
|
# for the very first start (with a strong password), then remove them. Do
|
||||||
# CSRF Cookie httpOnly setting (default: true)
|
# not ship a real deployment with a default password.
|
||||||
# Set CSRF_HTTP_ONLY=false to allow JavaScript access for SPA CSRF patterns
|
|
||||||
CSRF_HTTP_ONLY: "false"
|
# Set the at-rest secrets key OUTSIDE the database (SimpleFIN token, OIDC
|
||||||
|
# secret, SMTP password). Without it the app stores an auto-key in the DB
|
||||||
|
# next to the ciphertext — anyone with DB/backup read access can decrypt.
|
||||||
|
# Generate: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
|
||||||
|
# TOKEN_ENCRYPTION_KEY: "replace-with-a-long-random-string"
|
||||||
|
|
||||||
|
# Behind a reverse proxy (Portainer/Traefik/nginx): trust it so req.secure
|
||||||
|
# (Secure cookies) and req.ip (rate limits, audit + login history) are correct.
|
||||||
|
TRUST_PROXY: "true"
|
||||||
|
|
||||||
|
# CSRF cookie httpOnly (default: true). The SPA reads the token from
|
||||||
|
# GET /api/auth/csrf-token into memory, so it never needs JS cookie access —
|
||||||
|
# keep true to keep the token off the XSS-accessible surface.
|
||||||
|
CSRF_HTTP_ONLY: "true"
|
||||||
# CSRF Cookie sameSite setting (default: strict)
|
# CSRF Cookie sameSite setting (default: strict)
|
||||||
# Set CSRF_SAME_SITE=lax for SPA cross-site scenarios
|
# Set CSRF_SAME_SITE=lax for SPA cross-site scenarios
|
||||||
CSRF_SAME_SITE: "strict"
|
CSRF_SAME_SITE: "strict"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
"version": "0.40.0",
|
"version": "0.40.0",
|
||||||
"description": "Monthly bill tracking system",
|
"description": "Monthly bill tracking system",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.18.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:api": "node --watch server.js",
|
"dev:api": "node --watch server.js",
|
||||||
"dev:ui": "vite",
|
"dev:ui": "vite",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue