# 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.

layer 01

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.
Dockerfile.app.autogenerated / lines 1 to 11 github
# ---- 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
layer 02

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.
Dockerfile.app.autogenerated / lines 13 to 39 github
# ---- 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
layer 03

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.
Dockerfile.app.autogenerated / lines 41 to 75 github
# ---- 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
layer 04

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.
Dockerfile.app.autogenerated / lines 77 to 105 github
# ---- 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 changeLayers 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