2026-05-12 01:57:55 -05:00
|
|
|
# Build stage
|
|
|
|
|
FROM node:20-alpine AS builder
|
|
|
|
|
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
# Copy package files first for layer caching
|
|
|
|
|
COPY package.json package-lock.json* ./
|
|
|
|
|
|
2026-05-17 15:48:43 -05:00
|
|
|
# Install build tools for native modules (better-sqlite3)
|
|
|
|
|
RUN apk add --no-cache python3 make g++
|
|
|
|
|
|
2026-05-12 01:57:55 -05:00
|
|
|
# Install all dependencies for build
|
|
|
|
|
RUN npm ci
|
|
|
|
|
|
|
|
|
|
# Copy source files
|
|
|
|
|
COPY . .
|
|
|
|
|
|
2026-06-14 15:37:26 -05:00
|
|
|
# Public Vite values are compiled into the frontend bundle at build time.
|
|
|
|
|
ARG VITE_RECAPTCHA_SITE_KEY=
|
|
|
|
|
ENV VITE_RECAPTCHA_SITE_KEY=$VITE_RECAPTCHA_SITE_KEY
|
|
|
|
|
|
fix(seo): the production sitemap had no dates, and the build context had secrets
Three things, all in the path between this repository and the running image.
Closes #225, #224 and #223.
1. THE PRODUCTION SITEMAP CARRIED NO LASTMOD AT ALL. Dates come from git
history, and the image build cannot see git: .dockerignore excludes .git and
node:alpine has no git binary. prerender.js read the failure into an empty
catch commented "git unavailable or file untracked", so all 18 URLs came out
undated while the build printed a success line. Local builds looked perfect,
which is why nobody caught it.
release.sh now computes the map where git exists, passes it as the
SITEMAP_LASTMOD build arg, and then asks the built image whether its sitemap
has dates, refusing to publish one that does not. prerender prints the count
on every run, so "18 URLs, 0 dated" can never again read as success. The
route-to-source map moved into scripts/lib/routes.js, where a service page
now also counts its own content file, so editing one page's copy moves that
page's date and no other.
Proven: an image built with the arg carries 18 lastmod entries; a build with
git deliberately unreadable and no arg reports "18 URLs, 0 carrying a
lastmod" and warns.
2. THE DOCKER BUILD CONTEXT CARRIED CLIENT MATERIAL AND LIVE SECRETS. .drop/,
zoho.md (the reCAPTCHA secret and the Zoho tokens), Levi.md and two 30 MB
zips were all sent to the daemon on every build, along with four agent
workspaces. The final image copies only built output, so none of it ever
shipped, but one careless COPY would have changed that. Proven by listing the
context from inside a throwaway image: before, all of it; after, none of it.
3. UNTRACKED FILES PASSED SILENTLY. docker build packs the working tree, so an
untracked module the code imports produces an image that works and a tag that
cannot rebuild it. release.sh now refuses while untracked files are present,
and pre-commit's note counts them too.
#223 also claimed post-commit hides a refused push. It does not: it printed
"push was refused. The commit is safe locally and the branch is now ahead."
during this batch. The issue was corrected on the tracker rather than acted on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:58:37 -05:00
|
|
|
# Sitemap dates, computed by scripts/release.sh where git exists. This build
|
|
|
|
|
# has no git: .dockerignore excludes .git and this image ships no git binary.
|
|
|
|
|
# Without this the sitemap goes out with no lastmod at all, which is what
|
|
|
|
|
# production served for months.
|
|
|
|
|
ARG SITEMAP_LASTMOD=
|
|
|
|
|
ENV SITEMAP_LASTMOD=$SITEMAP_LASTMOD
|
|
|
|
|
|
2026-05-12 01:57:55 -05:00
|
|
|
# Build the frontend
|
|
|
|
|
RUN npm run build
|
|
|
|
|
|
2026-05-17 16:34:36 -05:00
|
|
|
# Native modules stage — compile better-sqlite3 in a dedicated stage
|
|
|
|
|
FROM node:20-alpine AS native-deps
|
|
|
|
|
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
COPY package.json package-lock.json* ./
|
|
|
|
|
|
|
|
|
|
RUN apk add --no-cache python3 make g++
|
|
|
|
|
RUN npm ci --omit=dev
|
|
|
|
|
|
2026-05-12 01:57:55 -05:00
|
|
|
# Production stage
|
|
|
|
|
FROM node:20-alpine AS runner
|
|
|
|
|
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
# Create non-root user for security (consistent UID/GID 1001)
|
|
|
|
|
RUN addgroup -g 1001 -S nodejs && \
|
|
|
|
|
adduser -S nodejs -u 1001 -G nodejs
|
|
|
|
|
|
|
|
|
|
# Set environment
|
|
|
|
|
ENV NODE_ENV=production
|
2026-05-13 18:37:32 -05:00
|
|
|
ENV SERVER_PORT=3001
|
|
|
|
|
ENV RATE_LIMIT_PER_MINUTE=5
|
fix(infra): queuenorth.com is the production origin, and it is this deployment
_null confirmed queuenorth.com as the permanent public origin, which settles
#212 and exposed a wrong claim written earlier the same day.
docs/OPERATIONS.md said queuenorth.com "is not this deployment ... do not
diagnose against it". That came from a DNS lookup and an assumption. It is this
deployment: both hostnames serve the identical bundle and this server's own
/api/health shape, 24.41.108.95 is this network's own public IP, and both reach
qn-website-dev on nebula — queuenorth.com through nginx-proxy-manager on
thor/exodus, qn.isnull.dev through Cloudflare. Two front doors, one container,
no non-production environment.
That is the worst direction for a runbook to be wrong in, so the correction
quotes the wrong sentence rather than replacing it silently. The QA Round 0
table likewise gained the production observations as extra rows instead of
having its originals rewritten.
Dockerfile: the CORS_ORIGIN fallback was '*'. The server sets credentials:true
and browsers reject '*' with credentials outright, so that fallback would have
broken every form rather than over-permitting. Now the real origin.
healthcheck.sh and preflight.sh now watch production by default, with the
second front door reachable through their env overrides — the two ingresses
terminate TLS in different places and can rot independently.
Also fills a gap adoption left explicitly undone: the deploy path is a Portainer
stack, id 58 on nebula, found from the container's own compose labels.
OPERATIONS.md documents it, including that the stack file is a separate copy
from this repository's docker-compose.yml and the two have already drifted.
That drift is all that remains of #212 — one trailing slash on line 21 of the
stack file. Left in place: nothing is broken today, and fixing it recreates the
container and takes both front doors down together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:08:12 -05:00
|
|
|
# The production origin, not '*'. A wildcard here is not merely loose: the
|
|
|
|
|
# server sets credentials:true, and browsers reject '*' with credentials
|
|
|
|
|
# outright — so the fallback would break every form rather than over-permit.
|
|
|
|
|
ENV CORS_ORIGIN=https://queuenorth.com
|
2026-05-13 18:37:32 -05:00
|
|
|
ENV LOG_LEVEL=info
|
2026-06-14 16:08:29 -05:00
|
|
|
ENV ZOHO_FORWARDING_MODE=webtolead
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_ENABLED=false
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_XNQSJSDP=
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_XMIWTLD=
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_RETURN_URL=null
|
|
|
|
|
ENV ZOHO_WEBTOLEAD_ZC_GAD=
|
2026-05-13 18:37:32 -05:00
|
|
|
ENV ZOHO_ENABLED=false
|
|
|
|
|
ENV ZOHO_API_DOMAIN=https://www.zohoapis.com
|
2026-05-17 18:37:10 -05:00
|
|
|
ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
|
2026-05-13 18:37:32 -05:00
|
|
|
ENV ZOHO_CLIENT_ID=
|
|
|
|
|
ENV ZOHO_CLIENT_SECRET=
|
|
|
|
|
ENV ZOHO_REFRESH_TOKEN=
|
2026-05-17 18:37:10 -05:00
|
|
|
ENV ZOHO_CASES_ENABLED=false
|
2026-06-14 15:37:26 -05:00
|
|
|
ENV RECAPTCHA_ENABLED=false
|
|
|
|
|
ENV RECAPTCHA_SECRET_KEY=
|
|
|
|
|
ENV RECAPTCHA_MIN_SCORE=0.5
|
2026-05-12 01:57:55 -05:00
|
|
|
|
|
|
|
|
# Create app directory structure
|
|
|
|
|
RUN mkdir -p /app/db /app/logs
|
|
|
|
|
|
2026-05-17 14:44:34 -05:00
|
|
|
# Set permissions for db directory (before USER switch)
|
|
|
|
|
RUN chown -R nodejs:nodejs /app/db /app/logs
|
2026-05-12 01:57:55 -05:00
|
|
|
|
2026-05-12 02:04:52 -05:00
|
|
|
# Copy from builder - built artifacts and package manifests
|
2026-05-12 01:57:55 -05:00
|
|
|
COPY --from=builder /app/package.json /app/package-lock.json* ./
|
|
|
|
|
COPY --from=builder /app/dist ./dist
|
|
|
|
|
COPY --from=builder /app/server ./server
|
|
|
|
|
|
2026-05-17 16:34:36 -05:00
|
|
|
# Copy compiled native modules from native-deps stage (no build tools in final image)
|
|
|
|
|
COPY --from=native-deps /app/node_modules ./node_modules
|
2026-05-12 01:57:55 -05:00
|
|
|
|
2026-08-18 02:34:27 -05:00
|
|
|
# Image metadata. org.opencontainers.image.version is the one that matters
|
|
|
|
|
# operationally: scripts/status.sh --deployed-version reads exactly this label,
|
|
|
|
|
# and without it the honest answer to "which version is running?" is "unknown" —
|
|
|
|
|
# which is what it reported on 2026-08-18, leaving the digest as the only way to
|
|
|
|
|
# tell one deploy from another. docs/OPERATIONS.md step 3 depends on it.
|
2026-09-10 06:01:25 -05:00
|
|
|
ARG APP_VERSION=0.9.7
|
2026-08-18 02:34:27 -05:00
|
|
|
LABEL org.opencontainers.image.version="$APP_VERSION" \
|
|
|
|
|
org.opencontainers.image.title="Queue North Website" \
|
|
|
|
|
org.opencontainers.image.source="https://dream.scheller.ltd/null/Queue-North-Website"
|
|
|
|
|
|
2026-05-12 01:57:55 -05:00
|
|
|
# Expose backend port
|
|
|
|
|
EXPOSE 3001
|
|
|
|
|
|
2026-05-17 14:44:34 -05:00
|
|
|
# Switch to non-root user (standard approach, no su-exec needed)
|
|
|
|
|
USER nodejs
|
|
|
|
|
|
2026-05-12 01:57:55 -05:00
|
|
|
# Health check using Node 20 built-in fetch (no wget required)
|
|
|
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
2026-05-17 21:34:39 -05:00
|
|
|
CMD node -e "fetch('http://localhost:3001/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
2026-05-12 01:57:55 -05:00
|
|
|
|
2026-05-17 14:44:34 -05:00
|
|
|
# Run the Express server
|
2026-05-12 01:57:55 -05:00
|
|
|
CMD ["node", "server/index.js"]
|