# How FreeLLMAPI's Multi-Stage Dockerfile Optimizes the Production Build

> Learn how FreeLLMAPI's multi-stage Dockerfile optimizes production builds by isolating dependencies and creating a secure, minimal image for efficient deployment.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: internals
- Published: 2026-08-31

---

**The FreeLLMAPI repository employs a three-stage Docker build (`deps`, `build`, and `runtime`) that isolates build toolchains, prunes development dependencies, and executes as a non-root user to produce a minimal, secure production image.**

FreeLLMAPI leverages Docker's multi-stage build capabilities to separate dependency installation, compilation, and runtime execution into distinct layers. This architecture ensures that heavy build tools and native module compilers never reach the final production container, significantly reducing image size and attack surface. By strategically ordering build instructions and pruning unnecessary packages, the `Dockerfile` creates a lean deployment artifact optimized for production environments.

## The Three-Stage Docker Build Architecture

The multi-stage Dockerfile divides the build process into three specialized stages: `deps`, `build`, and `runtime`. Each stage focuses on a specific task, allowing Docker to cache intermediate layers while excluding unnecessary files from the final image.

### Stage 1: Dependency Caching (deps)

The `deps` stage installs only the build-time dependencies required to compile native modules such as `better-sqlite3`. It copies all [`package.json`](https://github.com/tashfeenahmed/freellmapi/blob/main/package.json) files—including those in [`server/package.json`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/package.json) and [`client/package.json`](https://github.com/tashfeenahmed/freellmapi/blob/main/client/package.json)—before running `npm ci`.

This stage isolates the heavy toolchain—including Python, make, and g++—ensuring these compilers never appear in the runtime image. By copying dependency manifests early in the build process, Docker can reuse the layer cache for subsequent builds unless the [`package.json`](https://github.com/tashfeenahmed/freellmapi/blob/main/package.json) files themselves change.

### Stage 2: Compilation and Optimization (build)

The `build` stage inherits the prepared `node_modules` from the `deps` stage and copies the entire repository. It executes `npm run build` to generate compiled TypeScript output and then runs `npm prune --omit=dev` to remove development packages.

This pruning step is critical for multi-stage Dockerfile optimization because it strips testing frameworks, TypeScript compilers, and other devDependencies before files reach the runtime stage. Only production code and compiled assets persist, ensuring the subsequent `runtime` stage receives a minimal set of dependencies.

### Stage 3: Production Runtime (runtime)

The final `runtime` stage starts from a clean Node.js image defined by the `${NODE_IMAGE}` argument and copies only essential files: root [`package.json`](https://github.com/tashfeenahmed/freellmapi/blob/main/package.json), the trimmed `node_modules`, compiled server and client assets, and select static files such as [`desktop/package.json`](https://github.com/tashfeenahmed/freellmapi/blob/main/desktop/package.json).

The Dockerfile places `ARG FREELLMAPI_COMMIT_SHA` after heavy `COPY` instructions (lines 64‑65) to maximize layer caching. When source code changes occur, earlier layers containing `node_modules` remain cached because the commit SHA argument appears in later instructions. The container then switches to a non-root `node` user to enhance security.

## Security and Runtime Optimizations

Beyond size reduction, the production build implements security best practices through its entrypoint script and user privilege management.

### Non-Root User Execution

The runtime stage configures the container to run as a dedicated `node` user rather than root. However, the [`docker-entrypoint.sh`](https://github.com/tashfeenahmed/freellmapi/blob/main/docker-entrypoint.sh) script initially executes with root privileges to perform a `chown` operation on persistent data directories—a requirement for many PaaS platforms—before dropping privileges to the `node` user prior to starting the application (as seen in lines 55‑60 of the entrypoint script).

### Health Checks and Volume Handling

The Dockerfile defines a **HEALTHCHECK** instruction to verify service availability without requiring external monitoring tools. Notably, the configuration omits the `VOLUME` declaration for the data directory, leaving volume management to deployment orchestration rather than creating unwanted anonymous volumes during the build process.

## Building the Production Image

To leverage these optimizations, build the image with specific arguments corresponding to the `ARG` statements defined at lines 3‑4 and 64‑65 of the `Dockerfile`:

```bash
docker build \
  --build-arg NODE_IMAGE=node:20-bookworm-slim \
  --build-arg FREELLMAPI_COMMIT_SHA=$(git rev-parse HEAD) \
  -t freellmapi:latest .

```

Run the container with environment variables matching the production configuration defined at line 86:

```bash
docker run -d \
  -p 3001:3001 \
  -e PORT=3001 \
  -e NODE_ENV=production \
  --name freellmapi \
  freellmapi:latest

```

For persistent SQLite storage across container restarts, mount a named volume that the entrypoint script will properly permission:

```bash
docker volume create freellmapi-data
docker run -d \
  -p 3001:3001 \
  -v freellmapi-data:/app/server/data \
  freellmapi:latest

```

The [`docker-entrypoint.sh`](https://github.com/tashfeenahmed/freellmapi/blob/main/docker-entrypoint.sh) script referenced at line 71 automatically adjusts ownership of mounted directories before the application starts.

## Summary

- **Three-stage isolation** (`deps`, `build`, `runtime`) ensures build tools and native compilers never reach the production image, reducing both size and attack surface.
- **Layer caching optimization** via strategic placement of `ARG FREELLMAPI_COMMIT_SHA` prevents code changes from invalidating expensive `npm ci` operations.
- **Dependency pruning** using `npm prune --omit=dev` in the build stage eliminates development packages before they reach the runtime container.
- **Privilege separation** in the entrypoint script balances PaaS compatibility (temporary root for `chown`) with security (non-root `node` user for application execution).

## Frequently Asked Questions

### Why does FreeLLMAPI use three stages instead of a single stage?

Single-stage builds include Python compilers, make, g++, and development dependencies in the final image. The FreeLLMAPI multi-stage Dockerfile optimization separates these concerns, ensuring the production image contains only compiled application code and production `node_modules`. This approach typically reduces image size by hundreds of megabytes and eliminates security vulnerabilities associated with unused build tools.

### How does the Dockerfile handle native module compilation?

Native modules such as `better-sqlite3` require Python and build toolchains installed in the `deps` stage. The compiled binaries are copied to the `build` and `runtime` stages, but the heavy toolchains themselves are excluded from the final image. This allows FreeLLMAPI to use performant native bindings while maintaining a minimal production footprint.

### What is the purpose of the FREELLMAPI_COMMIT_SHA build argument?

This argument injects version metadata into the image at lines 64‑65 without disrupting layer caching. By placing it after the heavy `COPY` operations for `node_modules` and compiled assets, Docker can reuse cached layers for dependency installation even when the Git SHA changes, significantly speeding up CI/CD pipelines during code-only updates.

### How does the entrypoint script improve container security?

The [`docker-entrypoint.sh`](https://github.com/tashfeenahmed/freellmapi/blob/main/docker-entrypoint.sh) executes with root privileges only long enough to set ownership permissions on persistent data directories using `chown`. It then immediately drops to the unprivileged `node` user before executing the main application process. This pattern satisfies PaaS platform requirements for volume permissions while maintaining the security benefits of non-root container execution.