Slim the Docker image and clean up after every deploy
Deploy Vault CMS / deploy (push) Successful in 1m15s

- 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 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-11-11 20:43:34 +02:00
parent 3083a48b5f
commit ec125261e4
5 changed files with 35 additions and 12 deletions
+16 -2
View File
@@ -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
# 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
+10 -7
View File
@@ -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"]
+1 -1
View File
@@ -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 });
}
}
+5 -2
View File
@@ -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) },
+3
View File
@@ -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: [
{