recipes / container-images / tanstack-start
Containerize a TanStack Start app with Nitro
Add the Nitro Vite plugin, build a node-server bundle into .output, and run the entry with node in a distroless image.
TanStack Start builds with Vite. The Nitro plugin turns the server side into a self-contained Node.js bundle with its own graceful shutdown, so the Dockerfile copies one folder.
vite.config.ts
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { nitro } from "nitro/vite";
import viteReact from "@vitejs/plugin-react";
export default defineConfig({
plugins: [tanstackStart(), nitro({ preset: "node-server" }), viteReact()],
});
vite build emits .output/server/index.mjs and .output/public.
Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs24-debian12:nonroot
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOST=0.0.0.0
COPY --from=build --chown=nonroot:nonroot /app/.output ./.output
EXPOSE 3000
CMD [".output/server/index.mjs"]
Health route
// src/routes/api/healthz.ts
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/healthz")({
server: {
handlers: {
GET: () => new Response("ok"),
},
},
});
Notes
- Nitro’s node server listens on
PORTandHOST, handlesSIGTERMandSIGINT, and waits for in-flight requests up toNITRO_SHUTDOWN_TIMEOUT(default 30000 ms). Keep that belowterminationGracePeriodSeconds. - Runtime config: Nitro reads
NITRO_*env vars, everything else isprocess.envin server code. .outputcontains a copy of the dependencies it needs. Do not copynode_modules.
See it applied
- examples/tanstack-start in the repository
- examples/full - every recipe applied to one app
Updated 2026-09-10 · tags: tanstack, react, nitro, vite, dockerfile · edit on GitHub