Reduce Docker build memory use on small VPS
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 7m39s

This commit is contained in:
2026-10-14 17:04:26 +05:00
parent 3c3d27b35c
commit 07cd0993a0
5 changed files with 103 additions and 5 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"],

View File

@@ -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",

43
scripts/build-docker.mjs Normal file
View File

@@ -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,
);