From 07cd0993a0be8b804a67e4630e9379fe8067216c Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Wed, 14 Oct 2026 17:04:26 +0500 Subject: [PATCH] Reduce Docker build memory use on small VPS --- Dockerfile | 10 ++++---- docs/dokploy-deployment.md | 48 ++++++++++++++++++++++++++++++++++++++ next.config.ts | 6 +++++ package.json | 1 + scripts/build-docker.mjs | 43 ++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 scripts/build-docker.mjs diff --git a/Dockerfile b/Dockerfile index d44075c..21cd4ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,12 +18,12 @@ ENV NODE_ENV=production \ RUN if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY" ]; then \ echo "In Dokploy, set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY under Application > Environment > Build Time Arguments." >&2; \ exit 1; \ - fi && npm run build + fi && npm run build:docker -FROM node:22-bookworm-slim AS production-dependencies -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci --omit=dev --no-audit --no-fund +# Reuse the installation, and prune only after the build has completed. +# A second independent npm ci stage could overlap with compilation in BuildKit. +FROM builder AS production-dependencies +RUN npm prune --omit=dev --no-audit --no-fund FROM node:22-bookworm-slim AS runner WORKDIR /app diff --git a/docs/dokploy-deployment.md b/docs/dokploy-deployment.md index 69d863a..b18f4cc 100644 --- a/docs/dokploy-deployment.md +++ b/docs/dokploy-deployment.md @@ -43,6 +43,54 @@ Also set the following in Dokploy's regular runtime environment: Add any optional production variables from `.env.example` in Dokploy's environment settings. Do not upload `.env.local` or add it to the repository. +## Building on a 2 GiB VPS + +The Dockerfile uses `npm run build:docker`. It generates route types and runs +the full TypeScript check in a separate process before compiling with Webpack. +Next.js skips its duplicate TypeScript check only after the separate check +succeeds. The Docker build enables Webpack's build worker and memory +optimizations, and keeps page collection at one worker. Each build Node process +has a 640 MiB JavaScript heap ceiling; this is not a limit on the total memory +used by all processes or native libraries. + +Dependencies are installed once, then pruned after compilation. The dependency +installation for the final image no longer runs independently alongside the +application build. Normal `npm run build` retains its usual type checking and +bundler behavior. See Next.js's [memory usage +guide](https://nextjs.org/docs/app/guides/memory-usage) for the build options. + +In Dokploy, keep builds concurrency at **1** on this VPS. Dokploy, PostgreSQL, +Redis, Traefik, the running application, and the new build share the host's RAM. +Avoid starting a panorama render during a deployment. Application runtime +resource limits do not necessarily limit the Docker image build. + +If the host freezes, capture these read-only diagnostics from the VPS shell +before retrying. They print resource figures and Dokploy container state without +environment values, API keys, private addresses, or repository names: + +```sh +free -m +df -h / +vmstat 1 5 +journalctl -k --since '30 minutes ago' --no-pager -o cat | + awk '/oom-kill|Out of memory|Killed process/ {n++} END {print "Kernel OOM messages:", n+0}' +docker ps -aq --filter label=com.docker.swarm.service.name=dokploy | + xargs -r docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Started={{.State.StartedAt}} Finished={{.State.FinishedAt}}' +``` + +A startup log containing `Cancelled 1 deployments` is Dokploy's startup cleanup, +not proof of a deployment timeout. Correlate container start/finish times and +kernel OOM messages with the failed build. `OOMKilled=false` for Dokploy does not +rule out a different process being killed or host-wide memory pressure. Journal +permissions or missing history can also prevent the OOM count from being useful. + +If swap is absent and memory pressure is confirmed, adding swap can prevent +abrupt memory exhaustion, but it consumes disk space and can make builds slower. +Approve server configuration changes separately; the repository changes do not +create swap, restart services, or change server resource limits. If builds still +exceed available memory, move image compilation to the MacBook runner and use a +private image-transfer or private-registry workflow. + ## Gitea Actions secrets Add the following repository Actions secrets in Gitea. Use the HTTPS origin of diff --git a/next.config.ts b/next.config.ts index 1dcadb9..b2a11f6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,12 +1,18 @@ import type { NextConfig } from "next"; import { withWorkflow } from "workflow/next"; +// Set only by build-docker.mjs after the separate type check has succeeded. +const dockerTypechecked = process.env.MINEGEO_DOCKER_TYPECHECKED === "1"; const config: NextConfig = { poweredByHeader: false, cacheMaxMemorySize: 16 * 1024 * 1024, enablePrerenderSourceMaps: false, + typescript: { ignoreBuildErrors: dockerTypechecked }, experimental: { cpus: 1, preloadEntriesOnStart: false, + ...(dockerTypechecked + ? { webpackBuildWorker: true, webpackMemoryOptimizations: true } + : {}), }, outputFileTracingIncludes: { "/downloads": ["./public/tools/**/*.jar"] }, serverExternalPackages: ["pg"], diff --git a/package.json b/package.json index 32e211e..460609a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dev:web": "next dev --hostname 127.0.0.1", "prebuild": "node scripts/clean-build.mjs", "build": "next build", + "build:docker": "node scripts/build-docker.mjs", "start": "node scripts/supervise.mjs production", "setup": "node scripts/setup.mjs", "reprocess": "node scripts/reprocess.mjs", diff --git a/scripts/build-docker.mjs b/scripts/build-docker.mjs new file mode 100644 index 0000000..b682e43 --- /dev/null +++ b/scripts/build-docker.mjs @@ -0,0 +1,43 @@ +import { spawnSync } from "node:child_process"; + +// Release each child's memory before starting the next expensive build phase. +// In particular, do not type-check while a compiled bundler is still resident. +function run(label, args, typechecked = false) { + console.log(`[build:docker] ${label}`); + const started = Date.now(); + const child = spawnSync(process.execPath, args, { + stdio: "inherit", + env: { + ...process.env, + NODE_OPTIONS: "--max-old-space-size=640", + NEXT_TELEMETRY_DISABLED: "1", + MINEGEO_DOCKER_TYPECHECKED: typechecked ? "1" : "0", + }, + }); + if (child.error) throw child.error; + if (child.status !== 0) { + console.error( + `[build:docker] ${label} failed (${child.signal || `exit ${child.status}`}).`, + ); + process.exit(child.status || 1); + } + console.log( + `[build:docker] ${label} finished in ${Math.round((Date.now() - started) / 1000)}s.`, + ); +} + +run("Clean build cache", ["scripts/clean-build.mjs"]); +run("Generate route types", ["node_modules/next/dist/bin/next", "typegen"]); +run("Check TypeScript", [ + "node_modules/typescript/bin/tsc", + "--noEmit", + "--incremental", + "false", +]); +// Type errors fail the preceding step. Only this invocation skips the duplicate +// check; regular npm run build still performs Next.js's normal type checking. +run( + "Compile production application", + ["node_modules/next/dist/bin/next", "build", "--webpack"], + true, +);