-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathDockerfile
More file actions
81 lines (67 loc) · 3.73 KB
/
Copy pathDockerfile
File metadata and controls
81 lines (67 loc) · 3.73 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
# syntax=docker/dockerfile:1
# ---- Build stage ---------------------------------------------------------
# Alpine so the native module (better-sqlite3) is compiled against the same
# musl libc the runner uses.
FROM node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2 AS builder
WORKDIR /app
# Build tools for better-sqlite3's native addon.
RUN apk add --no-cache python3 make g++ libc6-compat
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
# Tests run in CI (once, natively) before any image is built/pushed — see
# .github/workflows. Building here only compiles.
# Next.js standalone output (see next.config.js: output: 'standalone').
RUN npm run build
# ---- Runtime stage -------------------------------------------------------
FROM node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2 AS runner
WORKDIR /app
# Image metadata + Unraid Docker UI hints (icon + WebUI button).
LABEL org.opencontainers.image.title="Keeparr" \
org.opencontainers.image.description="Plex-login web app to decide what media to keep and report what's reclaimable. Tags and reports only — never deletes." \
org.opencontainers.image.source="https://github.com/drohack/Keeparr" \
org.opencontainers.image.licenses="MIT" \
net.unraid.docker.icon="https://raw.githubusercontent.com/drohack/Keeparr/main/public/icon.png" \
net.unraid.docker.webui="http://[IP]:[PORT:3000]/"
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# SQLite db lives here; mount a volume at /data.
ENV DATA_DIR=/data
# Runtime UID/GID the app drops to (entrypoint runs as root, chowns /data, then
# drops via su-exec). Override to match your host — Unraid uses 99:100.
ENV PUID=1001
ENV PGID=1001
# su-exec lets the entrypoint drop root → PUID:PGID after fixing volume ownership.
RUN apk add --no-cache libc6-compat su-exec \
&& addgroup -g 1001 -S nodejs \
&& adduser -u 1001 -S nextjs -G nodejs \
&& mkdir -p /data \
&& chown -R nextjs:nodejs /data \
# Runtime is `node server.js` — the bundled package managers are never
# invoked. Drop npm/npx/yarn/corepack: their transitive deps carry CVEs and
# they're pure attack surface here.
&& rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
/usr/local/lib/node_modules/corepack /usr/local/bin/corepack \
/opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg
# Standalone server bundle + static assets (traced native modules included).
# public/ must be copied explicitly — Next's standalone output does NOT
# include it (PWA icons + any static asset 404 without this).
# No --chown: app files stay root-owned (world-readable, so the su-exec'd PUID
# can read them) but NOT writable by the app process — only /data is writable.
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
# Entrypoint auto-generates SESSION_SECRET into /data on first boot when the
# env var isn't provided (no required secrets at install — the Seerr pattern),
# fixes /data ownership to PUID:PGID, then drops root → that user via su-exec.
# It intentionally starts as root (no USER line) so it can chown the bind mount.
COPY --chmod=755 docker-entrypoint.sh /docker-entrypoint.sh
EXPOSE 3000
VOLUME ["/data"]
# Health check (works in the Unraid Docker UI too, not just compose).
# Uses $PORT so overriding it doesn't leave the container perpetually unhealthy.
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "fetch('http://localhost:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["node", "server.js"]