# syntax=docker/dockerfile:1.7

###############################################################################
# Linkdr — production image
#
# Four stages so that a code change does not reinstall the dependency tree:
# `deps` is keyed on the lockfile alone, and Docker reuses it until that file
# changes. Installing inside the same layer as the source would rebuild ~1GB of
# node_modules on every commit.
#
# Node 22 rather than 23: 22 is the active LTS line, 23 is not and stops getting
# fixes. package.json says >=20; this pins the version we actually ship.
###############################################################################

ARG NODE_VERSION=22.12.0-alpine

# ─────────────────────────────── base ───────────────────────────────
FROM node:${NODE_VERSION} AS base
# libc6-compat: several native-ish npm packages ship glibc builds and fail on
# musl without it. Cheap, and the failure it prevents is an obscure one.
RUN apk add --no-cache libc6-compat

# pnpm, pinned to the version in `packageManager` so the image builds with the
# same resolver the lockfile was written by.
#
# `corepack enable` alone is not enough: the corepack bundled with Node 22.12
# carries expired npm registry signing keys and dies with "Cannot find matching
# keyid" before it ever downloads pnpm. Updating corepack first refreshes those
# keys, and `prepare --activate` fetches the exact version rather than asking
# the registry what "latest" is at build time.
ARG PNPM_VERSION=9.15.4
RUN npm install -g corepack@latest &&     corepack enable &&     corepack prepare pnpm@${PNPM_VERSION} --activate
WORKDIR /app

# ─────────────────────────────── deps ───────────────────────────────
# Every package.json in the workspace, and nothing else. Adding a source file
# here would defeat the layer cache this stage exists for.
FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc* ./
COPY apps/web/package.json                 apps/web/
COPY packages/api/package.json             packages/api/
COPY packages/db/package.json              packages/db/
COPY packages/geocode/package.json         packages/geocode/
COPY packages/notify/package.json          packages/notify/
COPY packages/shared/package.json          packages/shared/
COPY packages/storage/package.json         packages/storage/

# --frozen-lockfile: a lockfile that does not match package.json is a build
# failure, not something to silently resolve differently than developers did.
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm config set store-dir /pnpm/store && \
    pnpm install --frozen-lockfile

# ─────────────────────────────── build ──────────────────────────────
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY --from=deps /app/packages ./packages
COPY . .

# NEXT_PUBLIC_* is inlined into the client bundle at BUILD time — reading it
# from the container's environment at runtime is too late. Anything the browser
# must know therefore has to arrive here as a build argument.
ARG NEXT_PUBLIC_APP_URL
ARG NEXT_PUBLIC_CITY_NAME
ARG NEXT_PUBLIC_CITY_LAT
ARG NEXT_PUBLIC_CITY_LNG
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL \
    NEXT_PUBLIC_CITY_NAME=$NEXT_PUBLIC_CITY_NAME \
    NEXT_PUBLIC_CITY_LAT=$NEXT_PUBLIC_CITY_LAT \
    NEXT_PUBLIC_CITY_LNG=$NEXT_PUBLIC_CITY_LNG \
    NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

ENV NEXT_TELEMETRY_DISABLED=1 \
    NODE_ENV=production

# Every page that touches the database is `force-dynamic`, so no database is
# needed to build. If that ever stops being true this line is where it breaks,
# loudly, rather than at deploy time.
RUN pnpm --filter @linkdr/web build

# ─────────────────────────────── tools ──────────────────────────────
# Migrations, seeding and the asset migration. These need tsx, drizzle-kit and
# the drizzle/ SQL folder, none of which belong in the image that serves
# traffic — so they get their own target rather than bloating the runtime.
#
# Run as a one-shot alongside the app (see docker-compose.dokploy.yml), not as
# a long-lived service:
#   docker compose run --rm migrate
FROM base AS tools
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/packages ./packages
COPY . .
ENV NODE_ENV=production
# Idempotent: drizzle records applied migrations, so re-running is a no-op and
# a restarted container cannot double-apply anything.
CMD ["pnpm", "--filter", "@linkdr/db", "migrate"]

# ────────────────────────────── runtime ─────────────────────────────
FROM base AS runtime
WORKDIR /app

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    PORT=3000 \
    HOSTNAME=0.0.0.0

RUN addgroup --system --gid 1001 nodejs && \
    adduser  --system --uid 1001 nextjs

# The standalone bundle ships its own minimal node_modules and server.js.
# `public` and `.next/static` are NOT included in it and must be copied
# separately, or the site renders with no CSS and no images.
COPY --from=build --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=build --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public

USER nextjs
EXPOSE 3000

# Talks to Postgres, so a container that cannot reach its database never enters
# rotation. start-period covers first boot; see app/api/health/route.ts.
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
    CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

CMD ["node", "apps/web/server.js"]
