# Understanding the Three-Stage Build Process in LunaTV's Dockerfile

> Discover LunaTV's Dockerfile three-stage build process. Minimize image size, optimize caching, and enhance security by isolating stages. Learn how it works.

- Repository: [MoonTechLab/LunaTV](https://github.com/MoonTechLab/LunaTV)
- Tags: internals
- Published: 2026-09-08

---

**The three-stage build process in LunaTV's Dockerfile separates dependency installation, application compilation, and runtime execution into isolated stages to minimize image size, optimize layer caching, and enforce security through least-privilege principles.**

The LunaTV repository (MoonTechLab/LunaTV) containerizes its Next.js application using a sophisticated multi-stage Dockerfile defined in the project's root `Dockerfile`. This architectural pattern ensures that build tools and development dependencies never contaminate the production runtime, resulting in compact, secure container images optimized for deployment.

## Breakdown of the Three Build Stages

The Dockerfile implements three distinct stages named `deps`, `builder`, and `runner`. Each stage inherits from `node:20-alpine` and performs a specific function in the build pipeline to isolate concerns.

### Stage 1: Dependency Caching (`deps`)

The first stage establishes a reproducible dependency layer by installing all Node.js packages—including `devDependencies`—in an isolated environment defined in lines 2-14.

```dockerfile
FROM node:20-alpine AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

```

By copying only [`package.json`](https://github.com/MoonTechLab/LunaTV/blob/main/package.json) and [`pnpm-lock.yaml`](https://github.com/MoonTechLab/LunaTV/blob/main/pnpm-lock.yaml) before running `pnpm install --frozen-lockfile`, Docker creates a cacheable layer that persists across builds when dependencies remain unchanged. This prevents redundant reinstallation of packages during source code modifications, significantly accelerating iterative development.

### Stage 2: Application Compilation (`builder`)

The second stage handles the computationally intensive task of compiling the Next.js application into a production-ready bundle, utilizing lines 15-30 from the source.

```dockerfile
FROM node:20-alpine AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV DOCKER_ENV=true
RUN pnpm run build

```

This stage pulls the pre-installed `node_modules` directory from the `deps` stage using `COPY --from=deps`, then injects the application source code. Setting `DOCKER_ENV=true` signals build-time optimizations specific to LunaTV, while `pnpm run build` generates the optimized output. Isolating compilation here ensures that build artifacts and heavy tooling never propagate to the final image.

### Stage 3: Minimal Runtime (`runner`)

The final stage produces a hardened, minimal runtime environment containing only the compiled application and necessary runtime assets, as configured in lines 31-58.

```dockerfile
FROM node:20-alpine AS runner
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/public ./public
USER nextjs
CMD ["node", "start.js"]

```

This stage extracts only the standalone bundle from `/app/.next/standalone`—generated by the standalone output configuration in [`next.config.js`](https://github.com/MoonTechLab/LunaTV/blob/main/next.config.js)—along with static assets from the `public` directory. The image runs under the non-root `nextjs` user, eliminating development tools, source code, and package managers to achieve a footprint of approximately tens of megabytes.

## Performance and Security Benefits

The three-stage architecture delivers measurable advantages across build velocity, security posture, and resource efficiency.

**Cache Efficiency**  
Separating dependency installation from compilation means that changes to application source code trigger only the `builder` and `runner` stages. The `deps` layer remains cached when [`package.json`](https://github.com/MoonTechLab/LunaTV/blob/main/package.json) and [`pnpm-lock.yaml`](https://github.com/MoonTechLab/LunaTV/blob/main/pnpm-lock.yaml) are stable, reducing build times from minutes to seconds during routine development cycles.

**Security Hardening**  
The final `runner` image excludes all build-time dependencies, compilers, and package managers that could serve as attack vectors. By enforcing the `USER nextjs` directive, the container executes with minimal privileges, adhering to the principle of least privilege and preventing privilege escalation attacks.

**Image Size Reduction**  
Copying only the `standalone` bundle and static assets—rather than the entire source tree and `node_modules`—dramatically reduces the final image size. This leads to faster deployment times, reduced storage costs, and quicker container startup in orchestration environments.

## Build Commands and CI/CD Integration

Execute the multi-stage build locally using standard Docker commands:

```bash
docker build -t lunatv:dev -f Dockerfile .
docker run -p 3000:3000 lunatv:dev

```

For production CI/CD pipelines, leverage build caching across stages using GitHub Actions:

```yaml
- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ghcr.io/moontechlab/lunatv:latest
    cache-from: type=registry,ref=ghcr.io/moontechlab/lunatv:cache
    cache-to: type=registry,ref=ghcr.io/moontechlab/lunatv:cache,mode=max

```

Verify the resulting image efficiency by inspecting the size:

```bash
docker image ls ghcr.io/moontechlab/lunatv:latest

```

The output should reflect a compact image—typically under 100 MB—containing only the runtime stage artifacts.

## Summary

- **Three-stage isolation** creates distinct environments for dependencies (`deps`), compilation (`builder`), and execution (`runner`) to prevent build tool contamination in production.
- **Layer caching** accelerates rebuilds by preserving the `pnpm install` layer when [`package.json`](https://github.com/MoonTechLab/LunaTV/blob/main/package.json) and [`pnpm-lock.yaml`](https://github.com/MoonTechLab/LunaTV/blob/main/pnpm-lock.yaml) remain unchanged across builds.
- **Security hardening** removes development dependencies and executes the container as the non-root `nextjs` user defined in the `Dockerfile`.
- **Minimal footprint** is achieved by copying only the Next.js standalone bundle from `.next/standalone` and static assets into the final image, excluding source code and build tools.

## Frequently Asked Questions

### Why does LunaTV use three stages instead of a single-stage build?

Single-stage builds combine dependency installation, compilation, and runtime into one image, unnecessarily including build tools and `devDependencies` in production. LunaTV's three-stage approach ensures the final `runner` image contains only the compiled standalone output and runtime necessities, reducing attack surface and storage requirements by excluding Node.js build tools and source code.

### How does the `deps` stage improve build performance?

The `deps` stage copies only [`package.json`](https://github.com/MoonTechLab/LunaTV/blob/main/package.json) and [`pnpm-lock.yaml`](https://github.com/MoonTechLab/LunaTV/blob/main/pnpm-lock.yaml) before executing `pnpm install --frozen-lockfile`. Docker caches this layer independently, meaning subsequent builds that modify source code but not dependencies can skip the installation step entirely. This cache isolation reduces build times significantly during iterative development where only application logic changes.

### What security measures are implemented in the `runner` stage?

The `runner` stage implements two critical security controls: it uses the `USER nextjs` directive to run the container as a non-root user, preventing privilege escalation attacks, and it exclusively copies production artifacts from the `standalone` output directory specified in [`next.config.js`](https://github.com/MoonTechLab/LunaTV/blob/main/next.config.js). By omitting the original source code, package manager metadata, and development dependencies, the stage minimizes the attack surface available to potential exploit attempts.

### What role does the [`start.js`](https://github.com/MoonTechLab/LunaTV/blob/main/start.js) file play in the runtime stage?

The [`start.js`](https://github.com/MoonTechLab/LunaTV/blob/main/start.js) script serves as the container's entry point, referenced in the `CMD ["node", "start.js"]` instruction within the `runner` stage. Located in the repository root as [`start.js`](https://github.com/MoonTechLab/LunaTV/blob/main/start.js), this file orchestrates the server initialization process for the standalone Next.js application, ensuring proper signal handling and environment configuration when the container executes in production environments.