build size reduse it

This commit is contained in:
2026-08-06 20:59:19 +05:30
parent 28258b5a69
commit 0ea52551aa
14 changed files with 160 additions and 10 deletions

48
.dockerignore Normal file
View File

@@ -0,0 +1,48 @@
# Docker build context excludes.
#
# WITHOUT this file, the `COPY . .` in the builder stage ships the host's
# .next/ (2.1 GB, almost all of it the Turbopack dev cache) and node_modules/
# (639 MB, with darwin-arm64 sharp binaries that are wrong for Alpine) into
# the build context. That is what filled the production disk.
# Dependencies — reinstalled from the lockfile in the deps stage
node_modules
.pnp
.pnp.*
.yarn
# Build output — regenerated by `npm run build` in the builder stage.
# .next/dev alone is 1.8 GB of dev-server cache that must never leave the host.
.next
out
build
dist
# VCS + local tooling
.git
.gitignore
.github
.claude
.vscode
.idea
# Incremental compiler state (253 KB and host-specific)
*.tsbuildinfo
next-env.d.ts
# Secrets — injected at runtime, never baked into a layer
.env
.env.*
*.pem
# Docs and infra that the build does not read
*.md
Dockerfile
.dockerignore
nginx.conf
# Noise
.DS_Store
coverage
npm-debug.log*
yarn-error.log*

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@
# production # production
/build /build
/dist
# misc # misc
.DS_Store .DS_Store

View File

@@ -83,6 +83,34 @@ Gray is the accent. The **only** colour permitted is semantic: success, warning,
- `StyleOverrides` supports structural pseudo-classes, not just interaction ones — - `StyleOverrides` supports structural pseudo-classes, not just interaction ones —
`':first-child'` emits correctly (used for the table first-column lead-in). `':first-child'` emits correctly (used for the table first-column lead-in).
## Deployment — never ship `.next/` wholesale
This filled the production disk and took the server down once. A working tree's `.next/`
reaches **2+ GB**, but almost none of it is runtime state:
| path | size (typical) | ships? |
|---|---|---|
| `.next/dev` | 1.8 GB | **no** — Turbopack dev-server cache from `npm run dev` |
| `.next/cache` | 120160 MB | **no** — incremental build cache, build host only |
| `.next/server`, `types`, `trace` | ~24 MB | **no** — inputs to the standalone trace, not read at runtime |
| `.next/standalone` | 54 MB | **yes** — server.js + traced node_modules |
| `.next/static` | 3 MB | **yes** — hashed client assets (nginx serves at `/_next/static/`) |
| `public` | 344 KB | **yes** |
Those last three are the entire payload: **~55 MB unpacked, 18 MB gzipped.**
- Non-Docker deploy: `npm run bundle``dist/loyaly-mer-login.tar.gz`. Never `scp -r .next`.
- Docker: `.dockerignore` excludes `node_modules` and `.next`. **Do not delete it** — without
it `COPY . .` pushes the host's 2 GB `.next` and 639 MB `node_modules` into the build
context, including darwin-arm64 sharp binaries that are wrong for Alpine.
- `npm run clean` drops `.next`, `dist` and the tsbuildinfo when the tree gets heavy.
**`CI_BUILD=1` in container builds.** Next 16.3 enables `turbopackFileSystemCacheForBuild`
by default; it writes 117 MB to `.next/cache` that only pays off when that directory is
restored between builds. Docker/Dokploy starts from a clean layer every time, so it is
written and never read. The Dockerfile sets `CI_BUILD=1`, which flips the flag off in
`next.config.ts`: **~20% less build CPU** (32s vs 4043s measured) and `.next` drops
202 MB → 86 MB. Leave it unset locally — repeat `npm run build` there does reuse the cache.
It changes no output: the deployable payload is 58 MB either way.
## Conventions ## Conventions
- Follow the Astryx rules above: no raw `<div>` for layout, no hardcoded hex/px, component - Follow the Astryx rules above: no raw `<div>` for layout, no hardcoded hex/px, component
props first then token-backed Tailwind utilities. props first then token-backed Tailwind utilities.

View File

@@ -1,10 +1,14 @@
# syntax=docker/dockerfile:1
# Stage 1: Install dependencies # Stage 1: Install dependencies
FROM node:22-alpine AS deps FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci # devDeps are required to build (typescript, tailwind, eslint-config-next).
# This whole stage is discarded — none of it reaches the runner.
RUN npm ci --no-audit --no-fund
# Stage 2: Build the Next.js application # Stage 2: Build the Next.js application
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
@@ -14,11 +18,16 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production ENV NODE_ENV=production
# Each Docker build starts from a clean layer, so Turbopack's .next/cache is
# written but never restored. Skipping it cuts ~20% of build CPU (the metric
# that matters on a 1-vCPU host) and 116 MB off this layer.
ENV CI_BUILD=1
RUN npm run build RUN npm run build
# Stage 3: Production runner with Next.js Standalone # Stage 3: Production runner with Next.js Standalone
FROM node:22-alpine AS runner FROM node:22-alpine AS runner
RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -26,10 +35,18 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
# Copy public static assets and standalone build output # Run as a non-root user; nextjs owns nothing it does not need to write.
COPY --from=builder /app/public ./public RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static # Copy public static assets and standalone build output.
# These three paths are the ENTIRE runtime payload (~57 MB). Never copy the
# whole .next/ directory here — .next/dev and .next/cache are build-host-only
# and account for ~1.96 GB.
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000 EXPOSE 3000

View File

@@ -11,6 +11,8 @@ const eslintConfig = defineConfig([
".next/**", ".next/**",
"out/**", "out/**",
"build/**", "build/**",
// Deploy artifact from `npm run bundle` — traced vendor code, not ours.
"dist/**",
"next-env.d.ts", "next-env.d.ts",
// Generated by `npm run theme:build` — edit src/theme/loyalyTheme.ts // Generated by `npm run theme:build` — edit src/theme/loyalyTheme.ts
// instead. The emitted .d.ts uses a triple-slash reference we do not own. // instead. The emitted .d.ts uses a triple-slash reference we do not own.

View File

@@ -2,6 +2,14 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
experimental: {
// Turbopack's build cache lives in .next/cache and only pays off when that
// directory survives between builds. A Docker/Dokploy build starts from a
// clean layer every time, so the cache is written and never read — ~117 MB
// of pure write cost per build. CI_BUILD is set in the Dockerfile only, so
// local `npm run build` keeps its warm cache.
turbopackFileSystemCacheForBuild: process.env.CI_BUILD !== "1",
},
allowedDevOrigins: ["192.168.0.117", "192.168.0.*", "192.168.1.*", "localhost", "127.0.0.1"], allowedDevOrigins: ["192.168.0.117", "192.168.0.*", "192.168.1.*", "localhost", "127.0.0.1"],
images: { images: {
remotePatterns: [ remotePatterns: [

View File

@@ -7,6 +7,8 @@
"build": "next build", "build": "next build",
"start": "next start -p 3100", "start": "next start -p 3100",
"lint": "eslint", "lint": "eslint",
"clean": "rm -rf .next dist tsconfig.tsbuildinfo",
"bundle": "bash scripts/bundle.sh",
"theme:build": "astryx theme build src/theme/loyalyTheme.ts", "theme:build": "astryx theme build src/theme/loyalyTheme.ts",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"dev:preview": "next dev" "dev:preview": "next dev"

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

49
scripts/bundle.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
#
# Produce the deployable artifact for a non-Docker deploy.
#
# Copying .next/ wholesale is what filled the production disk: a working tree's
# .next/ reaches 2+ GB, but only three paths are read at runtime —
# .next/standalone the server + its traced node_modules
# .next/static hashed client assets (served by nginx at /_next/static/)
# public unhashed public assets
# Everything else (.next/cache, .next/dev, .next/server, .next/types) is
# build-host state and must never be shipped.
#
# Usage: npm run bundle -> dist/loyaly-mer-login.tar.gz
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
OUT="dist"
STAGE="$OUT/bundle"
for p in .next/standalone .next/static public; do
if [ ! -d "$p" ]; then
echo "error: $p missing — run 'npm run build' first" >&2
exit 1
fi
done
rm -rf "$STAGE"
mkdir -p "$STAGE"
# standalone already contains server.js, package.json and traced node_modules,
# and expects static/ and public/ to sit beside it in the same layout.
cp -R .next/standalone/. "$STAGE/"
mkdir -p "$STAGE/.next"
cp -R .next/static "$STAGE/.next/static"
cp -R public "$STAGE/public"
TARBALL="$OUT/loyaly-mer-login.tar.gz"
rm -f "$TARBALL"
tar -czf "$TARBALL" -C "$STAGE" .
echo
echo "bundle: $(du -sh "$STAGE" | cut -f1) ($STAGE)"
echo "tarball: $(du -sh "$TARBALL" | cut -f1) ($TARBALL)"
echo
echo "deploy: scp $TARBALL <host>:/srv/ && tar -xzf loyaly-mer-login.tar.gz -C /srv/app"
echo "run: PORT=3000 HOSTNAME=0.0.0.0 NODE_ENV=production node server.js"