From ec125261e42709770a67bbdf13f991acbdaa6031 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Wed, 11 Nov 2026 20:43:34 +0200 Subject: [PATCH] Slim the Docker image and clean up after every deploy - Build Next.js as a standalone server and copy only that into the runtime image, instead of the whole node_modules tree - Mark three dynamic file reads with turbopackIgnore so the standalone trace does not pull in the entire project - Deploy workflow prunes unused images and build cache older than a day at the start and end of every run, including failed ones, and logs disk usage before building Failed builds were leaving their layers behind until the server's disk filled, which broke the deploy, the database and Gitea sign-in. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/deploy.yml | 18 ++++++++++++++++-- Dockerfile | 17 ++++++++++------- app/api/transcoder/upload/[videoId]/route.ts | 2 +- app/uploads/videos/[...path]/route.ts | 7 +++++-- next.config.ts | 3 +++ 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 2410796..113c5e9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -28,6 +28,20 @@ jobs: cd /opt/owicms - docker compose up -d --build + # Clean up afterwards whether or not the deploy succeeds; otherwise + # every failed build leaves its layers behind until the disk fills. + # Build cache used in the last day is kept, so routine deploys stay fast. + cleanup() { + docker image prune -f || true + docker builder prune -af --filter until=24h || true + } + trap cleanup EXIT - docker image prune -f \ No newline at end of file + # Start from a clean slate too, in case an earlier run was killed. + cleanup + + echo "Disk before build:" + df -h /opt || true + docker system df || true + + docker compose up -d --build \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 96a24c5..336ef67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,17 +36,20 @@ WORKDIR /app # Install dumb-init for proper signal handling RUN apk add --no-cache dumb-init -# Copy from builder: node_modules and .next build output. +# Copy the standalone server (only the files it actually uses, including the +# traced parts of node_modules), plus static assets, which it does not include. # Files stay root-owned so the app user cannot rewrite its own code; only the # Next.js cache directory is writable. -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/.next ./.next +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public -COPY --from=builder /app/package.json ./package.json RUN mkdir -p .next/cache && chown node:node .next/cache -# Set production environment +# Set production environment. HOSTNAME makes the server listen on all +# interfaces rather than only the container's hostname. ENV NODE_ENV=production +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 # Run as the unprivileged "node" user (uid 1000) instead of root USER node @@ -57,5 +60,5 @@ EXPOSE 3000 # Use dumb-init to handle signals properly ENTRYPOINT ["dumb-init", "--"] -# Start Next.js production server (directly, so npm doesn't need a writable home) -CMD ["node", "node_modules/next/dist/bin/next", "start"] +# Start the standalone Next.js server +CMD ["node", "server.js"] diff --git a/app/api/transcoder/upload/[videoId]/route.ts b/app/api/transcoder/upload/[videoId]/route.ts index c6f4654..05bdc40 100644 --- a/app/api/transcoder/upload/[videoId]/route.ts +++ b/app/api/transcoder/upload/[videoId]/route.ts @@ -57,7 +57,7 @@ export async function POST( // Security: path traversal guard. for (const p of [tempZipPath, tempDir, finalDir]) { - if (!path.resolve(p).startsWith(path.resolve(HLS_ROOT))) { + if (!path.resolve(/*turbopackIgnore: true*/ p).startsWith(path.resolve(HLS_ROOT))) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } } diff --git a/app/uploads/videos/[...path]/route.ts b/app/uploads/videos/[...path]/route.ts index 1114661..d057529 100644 --- a/app/uploads/videos/[...path]/route.ts +++ b/app/uploads/videos/[...path]/route.ts @@ -148,7 +148,10 @@ export async function GET( } if (range) { - const stream = fs.createReadStream(filePath, { start: range.start, end: range.end }); + const stream = fs.createReadStream(/*turbopackIgnore: true*/ filePath, { + start: range.start, + end: range.end, + }); return new NextResponse(Readable.toWeb(stream) as unknown as BodyInit, { status: 206, headers: { @@ -159,7 +162,7 @@ export async function GET( }); } - const stream = fs.createReadStream(filePath); + const stream = fs.createReadStream(/*turbopackIgnore: true*/ filePath); return new NextResponse(Readable.toWeb(stream) as unknown as BodyInit, { status: 200, headers: { ...headers, "Content-Length": String(stats.size) }, diff --git a/next.config.ts b/next.config.ts index a9db8e6..9e10fe1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,6 +15,9 @@ const securityHeaders = [ const nextConfig: NextConfig = { poweredByHeader: false, + // Emit a self-contained server with only the files it needs, so the Docker + // image does not have to ship the whole node_modules tree. + output: "standalone", images: { remotePatterns: [ {