Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
# Includes apps + Postgres + Redis + nightly R2 backup.
# Dev should use docker-compose.yml (infra only).

# NEXT_PUBLIC_* are inlined into the client bundle at build time, so they must
# be passed as build args (env_file only reaches runtime). Interpolated from
# the deploy env (Dokploy Environment tab / .env). Shared across all 3 apps.
x-public-build-args: &public-build-args
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
NEXT_PUBLIC_WEB_APP_URL: ${NEXT_PUBLIC_WEB_APP_URL:-}
NEXT_PUBLIC_MARKETING_URL: ${NEXT_PUBLIC_MARKETING_URL:-}
NEXT_PUBLIC_ADMIN_URL: ${NEXT_PUBLIC_ADMIN_URL:-}
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY:-}
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST:-}
NEXT_PUBLIC_GA_ID: ${NEXT_PUBLIC_GA_ID:-}
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:-}
NEXT_PUBLIC_BRAND_NAME: ${NEXT_PUBLIC_BRAND_NAME:-vibestack}
NEXT_PUBLIC_SUPPORT_EMAIL: ${NEXT_PUBLIC_SUPPORT_EMAIL:-}

services:
postgres:
image: postgres:17-alpine
Expand Down Expand Up @@ -48,6 +63,7 @@ services:
context: .
dockerfile: docker/Dockerfile.next
args:
<<: *public-build-args
APP_NAME: web
APP_PORT: 3001
restart: unless-stopped
Expand All @@ -73,6 +89,7 @@ services:
context: .
dockerfile: docker/Dockerfile.next
args:
<<: *public-build-args
APP_NAME: marketing
APP_PORT: 3000
restart: unless-stopped
Expand All @@ -91,6 +108,7 @@ services:
context: .
dockerfile: docker/Dockerfile.next
args:
<<: *public-build-args
APP_NAME: admin
APP_PORT: 3002
restart: unless-stopped
Expand Down
50 changes: 47 additions & 3 deletions docker/Dockerfile.next
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,68 @@ RUN pnpm install --frozen-lockfile
FROM base AS builder
ARG APP_NAME
ENV NEXT_TELEMETRY_DISABLED=1

# NEXT_PUBLIC_* are inlined into the client bundle at build time, so they must
# carry the REAL production values here (env_file only reaches runtime). They
# arrive as build args from docker-compose.prod.yml, which interpolates them
# from the deploy env. All optional except NEXT_PUBLIC_APP_URL.
ARG NEXT_PUBLIC_APP_URL
ARG NEXT_PUBLIC_WEB_APP_URL
ARG NEXT_PUBLIC_MARKETING_URL
ARG NEXT_PUBLIC_ADMIN_URL
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST
ARG NEXT_PUBLIC_GA_ID
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_BRAND_NAME
ARG NEXT_PUBLIC_SUPPORT_EMAIL
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL \
NEXT_PUBLIC_WEB_APP_URL=$NEXT_PUBLIC_WEB_APP_URL \
NEXT_PUBLIC_MARKETING_URL=$NEXT_PUBLIC_MARKETING_URL \
NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \
NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \
NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \
NEXT_PUBLIC_GA_ID=$NEXT_PUBLIC_GA_ID \
NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \
NEXT_PUBLIC_BRAND_NAME=$NEXT_PUBLIC_BRAND_NAME \
NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL

# Server vars are read from the container env at RUNTIME (env_file), never
# inlined. The build only needs shape-valid placeholders so env validation and
# module-load code (e.g. `new URL(APP_URL)` in @vibestack/auth) don't throw.
# These live only in the builder stage — the runner is a separate FROM and
# gets real values from env_file. No real secrets enter any image layer.
ENV DATABASE_URL=postgresql://build:build@localhost:5432/build \
BETTER_AUTH_SECRET=build_time_placeholder_secret_min_32_chars \
BETTER_AUTH_URL=http://localhost:3001 \
APP_URL=http://localhost:3001 \
CORS_ORIGIN=http://localhost:3001

COPY --from=deps /repo /repo
RUN pnpm --filter "./apps/${APP_NAME}" run build
# Guarantee a public/ dir exists (web/admin ship none) so the runner COPY
# below is unconditional — Dockerfile COPY has no shell-style optional copy.
RUN pnpm --filter "./apps/${APP_NAME}" run build \
&& mkdir -p "apps/${APP_NAME}/public"

FROM node:22-alpine AS runner
ARG APP_NAME
ARG APP_PORT
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=${APP_PORT}
# Persist APP_NAME so the runtime CMD can resolve the standalone server path.
ENV APP_NAME=${APP_NAME}
WORKDIR /app
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs

# Next.js standalone output
COPY --from=builder --chown=nextjs:nodejs /repo/apps/${APP_NAME}/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /repo/apps/${APP_NAME}/.next/static ./apps/${APP_NAME}/.next/static
COPY --from=builder --chown=nextjs:nodejs /repo/apps/${APP_NAME}/public ./apps/${APP_NAME}/public 2>/dev/null || true
COPY --from=builder --chown=nextjs:nodejs /repo/apps/${APP_NAME}/public ./apps/${APP_NAME}/public

USER nextjs
EXPOSE ${APP_PORT}
ENV HOSTNAME=0.0.0.0
CMD ["node", "apps/${APP_NAME}/server.js"]
# Shell form so $APP_NAME expands at runtime; `exec` hands PID 1 to node so it
# receives SIGTERM for graceful shutdown. Exec-form JSON would not expand vars.
CMD ["sh", "-c", "exec node apps/$APP_NAME/server.js"]
Loading