Reduce Docker build memory use on small VPS
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 7m39s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 7m39s
This commit is contained in:
10
Dockerfile
10
Dockerfile
@@ -18,12 +18,12 @@ ENV NODE_ENV=production \
|
|||||||
RUN if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY" ]; then \
|
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; \
|
echo "In Dokploy, set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY under Application > Environment > Build Time Arguments." >&2; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi && npm run build
|
fi && npm run build:docker
|
||||||
|
|
||||||
FROM node:22-bookworm-slim AS production-dependencies
|
# Reuse the installation, and prune only after the build has completed.
|
||||||
WORKDIR /app
|
# A second independent npm ci stage could overlap with compilation in BuildKit.
|
||||||
COPY package.json package-lock.json ./
|
FROM builder AS production-dependencies
|
||||||
RUN npm ci --omit=dev --no-audit --no-fund
|
RUN npm prune --omit=dev --no-audit --no-fund
|
||||||
|
|
||||||
FROM node:22-bookworm-slim AS runner
|
FROM node:22-bookworm-slim AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -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
|
Add any optional production variables from `.env.example` in Dokploy's
|
||||||
environment settings. Do not upload `.env.local` or add it to the repository.
|
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
|
## Gitea Actions secrets
|
||||||
|
|
||||||
Add the following repository Actions secrets in Gitea. Use the HTTPS origin of
|
Add the following repository Actions secrets in Gitea. Use the HTTPS origin of
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
import { withWorkflow } from "workflow/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 = {
|
const config: NextConfig = {
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
cacheMaxMemorySize: 16 * 1024 * 1024,
|
cacheMaxMemorySize: 16 * 1024 * 1024,
|
||||||
enablePrerenderSourceMaps: false,
|
enablePrerenderSourceMaps: false,
|
||||||
|
typescript: { ignoreBuildErrors: dockerTypechecked },
|
||||||
experimental: {
|
experimental: {
|
||||||
cpus: 1,
|
cpus: 1,
|
||||||
preloadEntriesOnStart: false,
|
preloadEntriesOnStart: false,
|
||||||
|
...(dockerTypechecked
|
||||||
|
? { webpackBuildWorker: true, webpackMemoryOptimizations: true }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
outputFileTracingIncludes: { "/downloads": ["./public/tools/**/*.jar"] },
|
outputFileTracingIncludes: { "/downloads": ["./public/tools/**/*.jar"] },
|
||||||
serverExternalPackages: ["pg"],
|
serverExternalPackages: ["pg"],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"dev:web": "next dev --hostname 127.0.0.1",
|
"dev:web": "next dev --hostname 127.0.0.1",
|
||||||
"prebuild": "node scripts/clean-build.mjs",
|
"prebuild": "node scripts/clean-build.mjs",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
"build:docker": "node scripts/build-docker.mjs",
|
||||||
"start": "node scripts/supervise.mjs production",
|
"start": "node scripts/supervise.mjs production",
|
||||||
"setup": "node scripts/setup.mjs",
|
"setup": "node scripts/setup.mjs",
|
||||||
"reprocess": "node scripts/reprocess.mjs",
|
"reprocess": "node scripts/reprocess.mjs",
|
||||||
|
|||||||
43
scripts/build-docker.mjs
Normal file
43
scripts/build-docker.mjs
Normal 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,
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user