# golden layers [l1 to l4]
Rebuilds that only redo what you changed.
Every service Flisk sets up builds in the same four layers. The slow, rarely changing steps sit at the bottom and are reused, so editing your code rebuilds only from the compile step up. The details below are for the curious.
What each layer holds
Lines below are from the real generated Dockerfile for reservation-api in the restaurant example, with their line numbers.
OS and runtime base
Starts from the stack's golden L1 image, which already has the operating system packages installed, then sets the runtime environment and the Bun cache directory.
- starts from
<project>-l1:latest- rebuilds when
- Only when the golden L1 image changes.
# ---- L1: os_base (Golden L1: tdk-restaurant-example-l1:latest) ----FROM tdk-restaurant-example-l1:latest AS l1_os_base# OS dependencies pre-installed in golden L1 imageWORKDIR /app# ---- L1: runtime_env ----# Shared runtime environmentENV NODE_ENV=productionENV BUN_INSTALL_CACHE_DIR=/cache/bun
Dependencies
Copies the workspace and service manifests, the lockfile, and the generated registry config, then installs dependencies against a shared BuildKit cache mount. No source code is copied in this layer.
- starts from
<project>-l2:latest- rebuilds when
- When package.json, bun.lock, or the generated .npmrc or bunfig change.
# ---- L2: dependency_manifest ----FROM tdk-restaurant-example-l2:latest AS l2_deps_manifestWORKDIR /app# Cache-busting mechanism - change this value to force dependency reinstallARG CACHE_BUSTER=2026-06-25-v1RUN echo "Cache buster: ${CACHE_BUSTER}"# Copy workspace root for monorepo resolutionCOPY package.json ./package.jsonCOPY services/guest/reservation-api/.autogenerated/.npmrc.docker.autogenerated /app/.npmrcCOPY services/guest/reservation-api/.autogenerated/bunfig.docker.autogenerated.toml /app/.bunfig.toml# Copy service manifest files for deterministic dependency layerWORKDIR /app/services/guest/reservation-apiCOPY services/guest/reservation-api/package.json ./package.jsonCOPY services/guest/reservation-api/.autogenerated/.npmrc.docker.autogenerated .npmrcCOPY bun.lock* ./COPY services/guest/reservation-api/.autogenerated/bunfig.docker.autogenerated.toml .bunfig.toml# Diagnostics suppressed (set TILT_DEBUG=true for verbose output)# Install dependencies (quiet by default; set TILT_DEBUG=true for verbose diagnostics)COPY shared-platform-engineering/docker-templates/install-deps.sh /usr/local/bin/install-deps.shRUN --mount=type=cache,target=/cache/bun,sharing=shared chmod +x /usr/local/bin/install-deps.sh && start_ts=$(date +%s) && /usr/local/bin/install-deps.sh && end_ts=$(date +%s) && echo "install-deps duration: $((end_ts-start_ts))s"RUN mkdir -p /app/node_modules/.bun
Build and prune
The compile stage reuses the node_modules resolved in L2 and runs bun run build. A second stage installs production dependencies only and deletes docs, tests, and source maps from node_modules.
- starts from
<project>-l3-backend:latest- rebuilds when
- The compile stage reruns when source in src/ changes; the prune stage only when package.json changes.
# ---- L3: backend_compiler ----FROM tdk-restaurant-example-l3-backend:latest AS l3_backend_buildWORKDIR /app/services/guest/reservation-api# Build argument to skip Infisical setup (for local dev / fast builds)ARG SKIP_INFISICAL_SETUP=0# Reuse resolved dependencies from l2_deps_manifest to avoid reinstalling in L3COPY --link --from=l2_deps_manifest /app/services/guest/reservation-api/node_modules ./node_modules# Bun isolated linker symlinks may resolve through /app/node_modules/.bunCOPY --link --from=l2_deps_manifest /app/node_modules/.bun /app/node_modules/.bun# Copy manifest files for dependency installation (in case L2 is stale)COPY services/guest/reservation-api/package.json ./package.jsonCOPY services/guest/reservation-api/.autogenerated/bunfig.docker.autogenerated.toml ./bunfig.tomlCOPY services/guest/reservation-api/.autogenerated/.npmrc.docker.autogenerated ./.npmrcCOPY services/guest/reservation-api/.autogenerated/tsconfig.docker.autogenerated.json ./tsconfig.jsonCOPY services/guest/reservation-api/.autogenerated/tsconfig.autogenerated.json ./.autogenerated/tsconfig.autogenerated.jsonCOPY services/guest/reservation-api/.autogenerated/*.ts ./.autogenerated/COPY services/guest/reservation-api/src ./srcENV NODE_ENV=productionRUN bun run build# Fix hono/cors subpath import for Bun runtime compatibilityRUN find ./dist -type f -name "*.js" -exec sed -i "s|from 'hono/cors'|from '../node_modules/hono/dist/middleware/cors/index.js'|g" {} + || true# ---- L3: production_purger ----FROM tdk-restaurant-example-l2:latest AS l3_production_purgerWORKDIR /app/services/guest/reservation-apiCOPY services/guest/reservation-api/package.json ./package.jsonCOPY services/guest/reservation-api/.autogenerated/bunfig.docker.autogenerated.toml ./bunfig.tomlCOPY services/guest/reservation-api/.autogenerated/.npmrc.docker.autogenerated ./.npmrcRUN --mount=type=cache,target=/cache/bun,sharing=shared bun install --production --cache-dir=/cache/bun# Option 2: Aggressive node_modules pruning to reduce size# NOTE: Keep .d.ts files - they are needed for Bun module resolution with subpath exportsRUN find node_modules -type f \( -name '*.md' -o -name '*.ts' ! -name '*.d.ts' -o -name '*.map' -o -name '*.test.js' -o -name '*.spec.js' -o -name 'README*' -o -name 'CHANGELOG*' -o -name 'LICENSE*' \) -deleteRUN find node_modules -type d \( -name '.bin' -o -name 'test' -o -name 'tests' -o -name '__tests__' -o -name 'spec' -o -name 'specs' -o -name '.github' -o -name 'docs' -o -name 'examples' \) -exec rm -rf {} + 2>/dev/null || true
Runtime
Copies only the pruned production node_modules and the built dist folder from L3, switches to the non-root bun user, and starts the service through the entrypoint script.
- starts from
<project>-l4-backend:latest- rebuilds when
- When anything it copies from L3 changes.
# ---- L4: backend_runtime ----FROM tdk-restaurant-example-l4-backend:latest AS l4_backend_runtimeWORKDIR /app/services/guest/reservation-api# Build argument to skip Infisical setup (for local dev / fast builds)ARG SKIP_INFISICAL_SETUP=0ENV NODE_ENV=productionCOPY --from=l3_production_purger /app/services/guest/reservation-api/node_modules ./node_modulesCOPY --from=l3_production_purger /app/services/guest/reservation-api/node_modules/.bun /app/node_modules/.bunCOPY --from=l3_backend_build /app/services/guest/reservation-api/dist ./distCOPY services/guest/reservation-api/package.json ./package.json# Fix Bun hoisted package symlinks that break when copied to DockerUSER rootCOPY shared-platform-engineering/docker-templates/bun-hoisted-symlink-fix.sh /usr/local/bin/bun-hoisted-symlink-fix.shRUN chmod +x /usr/local/bin/bun-hoisted-symlink-fix.sh && /usr/local/bin/bun-hoisted-symlink-fix.shUSER bunEXPOSE 4000COPY --chmod=0755 shared-platform-engineering/docker-templates/infisical-entrypoint.sh /entrypoint.shENV RESOURCE_NAME="reservation-api"ENV INFISICAL_SECRET_PATH="/services/reservation-api"ENV INFISICAL_TOKEN=${INFISICAL_TOKEN:-}ENV INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID:-}ENV INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET:-}ENV INFISICAL_API_URL=${INFISICAL_API_URL:-http://infisical:8080}ENV INFISICAL_PROJECT_ID=${INFISICAL_PROJECT_ID:-}ENV INFISICAL_ENVIRONMENT=${INFISICAL_ENVIRONMENT:-dev}ENV INFISICAL_ALLOW_FALLBACK=${INFISICAL_ALLOW_FALLBACK:-true}ENV RESOURCE_NAME=reservation-apiENTRYPOINT ["/entrypoint.sh"]CMD ["bun", "run", "start"]
What rebuilds when you change something
This follows from the order of the COPY lines above: Docker reruns a step only when a file it copies, or a step before it, has changed.
| You change | Layers Docker rebuilds |
|---|---|
| Source code in src/ | L1 cachedL2 cachedL3 rebuiltL4 rebuilt |
| package.json or bun.lock | L1 cachedL2 rebuiltL3 rebuiltL4 rebuilt |
| Nothing (a second build with no edits) | L1 cachedL2 cachedL3 cachedL4 cached |