# Creating a Docker image

> This guide will help you deploy the TanStack Start Drizzle SaaS boilerplate to any Docker-compatible platform. Easily self-host your application with Docker.

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/going-to-production/docker*

---

If you want to self-host your application, you can deploy it using Docker.

This guide will help you create a Docker image for the TanStack Start [Drizzle](/drizzle) SaaS boilerplate.

The app builds to a self-contained Nitro server bundle (`apps/web/.output`) that you run with `node apps/web/.output/server/index.mjs`.

{% callout title="Build-time vs runtime variables" %}
Public `VITE_*` variables are **inlined at build time** by Vite, so they must be present in the **build** environment (e.g. Docker build args / build-time env). Server-only secrets (unprefixed, e.g. `DATABASE_URL`, `BETTER_AUTH_SECRET`, `STRIPE_SECRET_KEY`) are read from `process.env` at **runtime** and can be passed via `--env-file` when you run the container. This means the same image cannot swap public `VITE_*` values per environment without a rebuild.
{% /callout %}

## 1. Create a Dockerfile

Create a `Dockerfile` in the root of the project. The following multi-stage build prunes the monorepo with Turborepo, installs dependencies, builds the `web` app, and ships only the Nitro output:

```dockerfile
# syntax=docker.io/docker/dockerfile:1
FROM node:20-alpine AS base

# Prune the monorepo for a smaller build context
FROM base AS pruner
RUN apk add --no-cache libc6-compat && \
    corepack enable pnpm && \
    npm install -g turbo
WORKDIR /app
COPY . .
RUN turbo prune web --docker

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat && corepack enable pnpm
WORKDIR /app
COPY --from=pruner /app/out/json/ ./
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile

# Build the app
FROM base AS builder
WORKDIR /app
RUN corepack enable pnpm && npm install -g turbo
COPY --from=deps /app ./
COPY --from=pruner /app/out/full/ ./
# Public VITE_* values are inlined here. Pass them as build args/env if needed.
RUN turbo run build --filter=web...

# Production image — copy the Nitro server output only
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production \
    PORT=3000 \
    HOST=0.0.0.0

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nodejs-user

# The Nitro build output is self-contained (server + public assets)
COPY --from=builder --chown=nodejs-user:nodejs /app/apps/web/.output ./apps/web/.output

USER nodejs-user
EXPOSE 3000

# Healthcheck hits the built-in /api/healthcheck route
HEALTHCHECK --interval=90s --timeout=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/healthcheck || exit 1

CMD ["node", "apps/web/.output/server/index.mjs"]
```

{% callout title="Bundled docker generator" %}
The repository ships a `pnpm turbo gen docker` generator (`turbo/generators/templates/docker/`) that produces this same Nitro-based Dockerfile (copying `apps/web/.output` and starting `.output/server/index.mjs`). It also adds the platform-specific `lightningcss` / `@tailwindcss/oxide` musl build deps. So you can either run the generator or copy the Dockerfile above by hand.
{% /callout %}

## 2. Build the Docker image

Run the following command to build the Docker image:

```bash
docker build -t tanstack-drizzle .
```

This command will build the Docker image and tag it as `tanstack-drizzle`.

If you need to pass public `VITE_*` values at build time, supply them as build environment variables (e.g. with `--build-arg` and a corresponding `ARG`/`ENV` in the builder stage) so they are inlined into the bundle.

## 3. Run the Docker container

### 3.1. Prepare a runtime env file

Create an env file with your server-only secrets for the production environment (for example `apps/web/.env.production.local`), containing values such as `DATABASE_URL`, `BETTER_AUTH_SECRET`, billing keys, and SMTP credentials.

### 3.2. Run the Docker container

Run the following command to run the Docker container:

```bash
docker run -p 3000:3000 --env-file apps/web/.env.production.local tanstack-drizzle
```

This command will run the Docker container and make the application available at `http://localhost:3000`.

## Deploying the Docker image to a container registry

You can deploy the Docker image to a container registry by running the following command:

```bash
docker push <your-docker-image>
```

This command will push the Docker image to the container registry.

When you deploy the Docker image to a container registry, you can pull it from the registry and run it on any platform that supports Docker.

If you're using the Github Container Registry, you can run the following command:

```bash
docker login ghcr.io
```

Then, tag your local image with the registry path:

```bash
docker tag tanstack-drizzle ghcr.io/your-username/tanstack-drizzle
```

Then, push the Docker image to the Github Container Registry:

```bash
docker push ghcr.io/your-username/tanstack-drizzle
```

Then, you can pull the Docker image from the Github Container Registry and run it on any platform that supports Docker:

```bash
docker pull ghcr.io/your-username/tanstack-drizzle
```

Then, you can run the Docker container:

```bash
docker run -p 3000:3000 --env-file apps/web/.env.production.local ghcr.io/your-username/tanstack-drizzle
```

This command will run the Docker container and make the application available at `http://localhost:3000`.

This workflow allows you to deploy the Docker image to a container registry, and then pull it from the registry and run it on any platform that supports Docker - such as a VPS, a cloud provider, or a container platform like Docker Swarm or Kubernetes.

Easy, right?
