Docker Deployment
Self-host Next Starter with Docker using the included Dockerfile, docker-compose, and Next.js standalone output mode. Covers building, environment variables, and running database migrations.
Overview
Next Starter is configured for self-hosted Docker deployment. The output: "standalone" setting in next.config.ts produces a minimal production image, and the included Dockerfile uses a multi-stage build to keep image size small.
Standalone Output Mode
In next.config.ts, the project sets:
const nextConfig: NextConfig = {
output: "standalone",
// ...
};This tells Next.js to produce a self-contained server.js file in .next/standalone that includes only the Node.js modules required to run the app. You do not need node_modules in the final image, only the files Next.js explicitly traces as needed.
Dockerfile Overview
The included Dockerfile uses three stages, all built on node:24.13.0-slim (Debian slim — pinned via an ARG NODE_VERSION so you can bump it in one place). BuildKit cache mounts speed up subsequent installs.
dependencies: Copies the package manifest plus every supported lockfile and installs dependencies (npm, yarn, or pnpm — pnpm is what this repo actually ships, the other branches are template fallbacks).
builder: Pulls the installed node_modules from the previous stage, copies the source, and runs the build. The build script in package.json also runs prisma generate and prisma migrate deploy before next build.
runner: The production image. It runs as the built-in node user, copies only the standalone output, static assets, and public/, and exposes port 3000.
# ============================================
# Stage 1: Dependencies Installation Stage
# ============================================
ARG NODE_VERSION=24.13.0-slim
FROM node:${NODE_VERSION} AS dependencies
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* pnpm-workspace.yaml* .npmrc* ./
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=/usr/local/share/.cache/yarn \
--mount=type=cache,target=/root/.local/share/pnpm/store \
if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm install --frozen-lockfile; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 2: Build Next.js application in standalone mode
# ============================================
FROM node:${NODE_VERSION} AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
RUN if [ -f package-lock.json ]; then \
npm run build; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn build; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm build; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 3: Run Next.js application
# ============================================
FROM node:${NODE_VERSION} AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder --chown=node:node /app/public ./public
RUN mkdir .next && chown node:node .next
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]docker-compose.yml for Local Development
The included docker-compose.yml runs PostgreSQL locally. It is intended for development, not for running the Next.js app itself in Docker during development (use pnpm dev for that).
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: next_starter
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:Start it with:
docker-compose up -dStop and remove data with:
docker-compose down -vBuilding the Docker Image
The build script (pnpm build) runs prisma generate, prisma migrate deploy, and the Next.js production build in sequence inside the container. Because .dockerignore excludes .env files (only .env.example is kept), you must pass build-time variables explicitly using --build-arg. The variables required at build time are:
DATABASE_URL: needed byprisma migrate deploy- All
NEXT_PUBLIC_*variables: inlined into the JavaScript bundle by Next.js (NEXT_PUBLIC_STORAGE_S3_CDN_URLis also evaluated innext.config.tsat import time)
Build-time vs Runtime Variables
Variables prefixed with NEXT_PUBLIC_ are inlined into the JavaScript bundle at build time. DATABASE_URL is also required at build time because prisma migrate deploy runs during the build step.
The included Dockerfile does not declare these build arguments. Before building, add the following ARG and ENV lines to the builder stage in your Dockerfile (between COPY . . and ENV NODE_ENV=production):
FROM node:${NODE_VERSION} AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
ARG DATABASE_URL
ARG NEXT_PUBLIC_BETTER_AUTH_URL
ARG NEXT_PUBLIC_STORAGE_S3_CDN_URL
ARG NEXT_PUBLIC_APP_NAME
ARG NEXT_PUBLIC_RECAPTCHA_SITE_KEY
ENV DATABASE_URL=$DATABASE_URL
ENV NEXT_PUBLIC_BETTER_AUTH_URL=$NEXT_PUBLIC_BETTER_AUTH_URL
ENV NEXT_PUBLIC_STORAGE_S3_CDN_URL=$NEXT_PUBLIC_STORAGE_S3_CDN_URL
ENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME
ENV NEXT_PUBLIC_RECAPTCHA_SITE_KEY=$NEXT_PUBLIC_RECAPTCHA_SITE_KEY
ENV NODE_ENV=productionThen build with:
docker build \
--build-arg DATABASE_URL=postgresql://user:password@your-db-host:5432/next_starter \
--build-arg NEXT_PUBLIC_BETTER_AUTH_URL=https://yourdomain.com \
--build-arg NEXT_PUBLIC_STORAGE_S3_CDN_URL=https://your-r2-cdn-domain.com \
--build-arg NEXT_PUBLIC_APP_NAME=your-app-name \
--build-arg NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your-site-key \
-t next-starter:latest .Environment Variables in Docker
The runner stage does not bake environment variables into the image. Pass them at runtime using --env-file or individual -e flags.
docker run \
--env-file .env.production \
-p 3000:3000 \
next-starter:latestYour .env.production file should contain all required variables:
DATABASE_URL="postgresql://user:password@your-db-host:5432/next_starter"
BETTER_AUTH_SECRET="your-32-character-secret"
BETTER_AUTH_URL="https://yourdomain.com"
NEXT_PUBLIC_BETTER_AUTH_URL="https://yourdomain.com"
PUBLIC_URL="https://yourdomain.com"
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_PUBLISHABLE_KEY="pk_live_..."
SMTP2GO_API_KEY="your-api-key"
SENDER_EMAIL="noreply@yourdomain.com"
STORAGE_S3_ENDPOINT="https://<account>.r2.cloudflarestorage.com"
STORAGE_S3_BUCKET="your-bucket"
STORAGE_S3_REGION="auto"
STORAGE_S3_KEY="your-access-key-id"
STORAGE_S3_SECRET="your-secret-access-key"
NEXT_PUBLIC_STORAGE_S3_CDN_URL="https://your-r2-cdn-domain.com"
NEXT_PUBLIC_APP_NAME="your-app-name"
NEXT_PUBLIC_RECAPTCHA_SITE_KEY="your-site-key"
RECAPTCHA_SECRET_KEY="your-secret-key"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"Production docker-compose Example
For self-hosting the full stack (app + database) on a single server:
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- internal
app:
image: next-starter:latest
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- .env.production
depends_on:
- postgres
networks:
- internal
networks:
internal:
volumes:
postgres_data:In .env.production, set DATABASE_URL to point at the postgres service using its service name as the hostname:
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}"Migrations in Production
Migrations run automatically as part of pnpm build (via prisma migrate deploy), so they are applied during the Docker build step before the app starts. No separate migration container is needed.
If you need to run migrations manually against a live database (for example, applying a schema change before deploying a new image), run the command from your project directory (locally or in CI) where the Prisma schema and migration files are available:
DATABASE_URL="postgresql://user:password@your-db-host:5432/next_starter" pnpm dlx prisma migrate deployThe production Docker image (runner stage) only contains the Next.js standalone output. It does not include the Prisma CLI, schema, or migration files, so you cannot run migrations from inside it.
prisma migrate deploy applies pending migrations without prompting. It is safe to run on each deploy: it is a no-op if the database is already up to date.
Deploy to Vercel
Step-by-step guide to deploying Next Starter on Vercel, including all required environment variables, Stripe webhook configuration, custom domain setup, and preview deployment best practices.
Testing
How Next Starter uses Playwright for end-to-end testing across authentication flows, protected routes, forms, SEO metadata, and smoke tests.