-
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathDockerfile
More file actions
142 lines (124 loc) · 6.46 KB
/
Copy pathDockerfile
File metadata and controls
142 lines (124 loc) · 6.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# Dockerfile
# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------
FROM node:24-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
# ---------------------------------------------------------------------------
# Stage 1 — Install dependencies
# python3/make/g++ are required for argon2 (native C++ addon)
# libc6-compat provides glibc shims some native modules expect on Alpine
# ---------------------------------------------------------------------------
FROM base AS deps
RUN apk add --no-cache python3 make g++ libc6-compat
WORKDIR /app
# pnpm-workspace.yaml is REQUIRED here, not optional. Since pnpm 11 it holds
# settings that package.json no longer carries: `overrides` (the esbuild
# security pin) and `minimumReleaseAgeExclude` (without which the
# supply-chain policy rejects recently-published packages the lockfile
# pins, and `--frozen-lockfile` fails outright).
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Stage 2 — Build the Next.js app
# ---------------------------------------------------------------------------
FROM base AS builder
ARG NEXT_PUBLIC_RELEASE_CHANNEL=stable
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_PUBLIC_RELEASE_CHANNEL=$NEXT_PUBLIC_RELEASE_CHANNEL
# Dummy DATABASE_URL so Next.js can evaluate route modules during build
# (In case DB is never actually queried at build time)
ENV DATABASE_URL=postgresql://build:build@localhost:5432/build
RUN pnpm build
# ---------------------------------------------------------------------------
# Stage 3 — Minimal deps for drizzle-kit
# ---------------------------------------------------------------------------
FROM base AS schema-deps
WORKDIR /schema-sync
# pnpm-workspace.yaml is REQUIRED here, not optional. Since pnpm 11 it holds
# settings that package.json no longer carries: `overrides` (the esbuild
# security pin) and `minimumReleaseAgeExclude` (without which the
# supply-chain policy rejects recently-published packages the lockfile
# pins, and `--frozen-lockfile` fails outright).
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Stage 4 — Production runner
# ---------------------------------------------------------------------------
FROM node:24-alpine AS runner
RUN apk add --no-cache libc6-compat bash && apk upgrade --no-cache \
&& rm -rf /usr/lib/node_modules /usr/local/lib/node_modules \
/usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# Non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
RUN mkdir -p /data/backups /data/logs && chown -R nextjs:nodejs /data
# --- Standalone server ---
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
# --- Drizzle schema-sync (for drizzle-kit push at startup) ---
RUN mkdir -p /schema-sync/src/lib
COPY --from=schema-deps /schema-sync/node_modules /schema-sync/node_modules
COPY --from=schema-deps /schema-sync/package.json /schema-sync/
COPY --from=builder /app/drizzle.config.ts /schema-sync/
COPY --from=builder /app/src/lib/db /schema-sync/src/lib/db
COPY --from=builder /app/tsconfig.json /schema-sync/
# --- Changelog (served via /api/changelog) ---
COPY --from=builder /app/CHANGELOG.md ./
# --- Entrypoint ---
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh
# --- Emergency master-password recovery ---
#
# The script is copied straight into the runner rather than traced into the
# standalone bundle: .dockerignore excludes scripts/ from the builder's context
# except for this one file, so outputFileTracingIncludes could never see it.
#
# require("argon2") already resolves — argon2 is on Next's builtin
# server-externals list, so the tracer emits /app/node_modules/argon2 for it.
#
# require("postgres") does NOT, and this COPY is why it does. Next bundles
# postgres.js into the server chunks, so nothing named "postgres" exists under
# /app/node_modules; that is the "Cannot find module 'postgres'" that broke a
# real password recovery. Marking it external in next.config.ts does not fix it
# either — see the comment there. So the CLI gets its own complete copy.
#
# COPY dereferences pnpm's symlink into a real directory, and postgres.js has
# zero runtime dependencies, so this one directory is the whole package
# including the cjs/ build that require() needs. The app server is untouched: it
# still uses its bundled copy and never resolves this one.
COPY --from=deps /app/node_modules/postgres /app/node_modules/postgres
COPY --chown=nextjs:nodejs scripts/recover.cjs /app/scripts/recover.cjs
# A real command instead of a path to memorise. /usr/local/bin is on PATH, the
# shim is root-owned 0755 so uid 1001 can execute it, and `docker exec` bypasses
# the entrypoint — so `docker exec -it tracker-tracker-app tt-recover` runs the
# CLI directly with no server side effects.
RUN printf '#!/bin/sh\nexec node /app/scripts/recover.cjs "$@"\n' > /usr/local/bin/tt-recover \
&& chmod 0755 /usr/local/bin/tt-recover
# --- Signed-buffer history repair ---
#
# Rewrites the historical buffer_bytes rows that the old negative-buffer clamp
# flattened to zero. It rides on the postgres COPY above and needs nothing else:
# buffer_bytes is not encrypted, so unlike tt-recover this tool loads no argon2,
# no crypto and no SESSION_SECRET.
#
# It ships in the same image as the signed-buffer fix on purpose — backfilling
# against the old code would repair history and then let the next poll write a
# fresh clamped zero over it. Dry run by default; --apply commits.
COPY --chown=nextjs:nodejs scripts/backfill-buffer.cjs /app/scripts/backfill-buffer.cjs
RUN printf '#!/bin/sh\nexec node /app/scripts/backfill-buffer.cjs "$@"\n' > /usr/local/bin/tt-backfill-buffer \
&& chmod 0755 /usr/local/bin/tt-backfill-buffer
USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
ENTRYPOINT ["./docker-entrypoint.sh"]