diff --git a/.dockerignore b/.dockerignore index b1ae973..910d721 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,7 @@ data/ *.log .git .gitignore +# Never bake secrets into the image (COPY . . would otherwise include a stray .env) +.env +.env.* +!.env.example diff --git a/Dockerfile b/Dockerfile index 6d34e91..bfe392d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,10 @@ WORKDIR /app # native build deps (better-sqlite3 etc) 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 ./ -RUN npm install +RUN npm ci # copy full project COPY . . @@ -44,5 +45,10 @@ ENV NODE_ENV=production \ EXPOSE 3000 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"] CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index be5199a..8c18c08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: bill-tracker: image: dream.scheller.ltd/null/billtracker:latest @@ -9,11 +7,24 @@ services: - "3030:3000" environment: - INIT_ADMIN_USER: admin - INIT_ADMIN_PASS: changeme123 - # CSRF Cookie httpOnly setting (default: true) - # Set CSRF_HTTP_ONLY=false to allow JavaScript access for SPA CSRF patterns - CSRF_HTTP_ONLY: "false" + # First-run admin bootstrap: set INIT_ADMIN_USER/INIT_ADMIN_PASS here ONLY + # for the very first start (with a strong password), then remove them. Do + # not ship a real deployment with a default password. + + # 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) # Set CSRF_SAME_SITE=lax for SPA cross-site scenarios CSRF_SAME_SITE: "strict" diff --git a/package.json b/package.json index 40f8db0..ea89e14 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.40.0", "description": "Monthly bill tracking system", "main": "server.js", + "engines": { + "node": ">=22.18.0" + }, "scripts": { "dev:api": "node --watch server.js", "dev:ui": "vite",