# syntax=docker/dockerfile:1

# Build stage: full dependency tree, compile TypeScript to dist/.
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
# tsc emits only .js, so the .sql migrations are copied by this step.
COPY scripts/copy-assets.mjs ./scripts/copy-assets.mjs
RUN npm run build

# Prune to runtime dependencies only, in its own stage so the build tree
# (typescript, @types) never reaches the final image.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

# Every extractor is pure JavaScript, so the runtime image needs no build
# toolchain — just a signal-forwarding init so SIGTERM reaches node.
RUN apk add --no-cache tini

COPY --from=deps  /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./

# The mirror, the state directory (a replaced session token) and the notes are
# the only writable paths. Creating them in the image with the right owner
# matters: Docker initialises a new named volume from the image directory,
# including its ownership, so without this the volume lands root-owned and the
# unprivileged user gets EACCES on every write — with the failure recorded
# rather than crashing, which makes it easy to miss. The state directory holds a
# credential and the notes are personal, so only their owner may enter either.
RUN mkdir -p /data/mirror /data/state /data/notes \
	&& chown -R node:node /data \
	&& chmod 700 /data/state /data/notes

# node:alpine ships an unprivileged `node` user.
USER node

EXPOSE 8080
ENV PORT=8080 BIND_HOST=0.0.0.0

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/bin/http.js"]
