feat: Docker production setup (Portainer + Traefik)
Adds multi-stage Dockerfile (runner + worker targets), docker-compose.prod.yml with Traefik labels, auto-migration on deploy, and next.config.ts standalone output mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+49
@@ -0,0 +1,49 @@
|
|||||||
|
FROM node:22-alpine AS base
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
# ── deps ─────────────────────────────────────────────────────────────────────
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# ── builder ───────────────────────────────────────────────────────────────────
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
# Dummy env so Next.js build doesn't fail on missing vars at build time.
|
||||||
|
# Real values are injected at runtime via docker-compose env.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV DATABASE_URL=postgresql://placeholder:placeholder@localhost:5432/curio
|
||||||
|
ENV REDIS_URL=redis://localhost:6379
|
||||||
|
ENV AUTH_SECRET=build-placeholder
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# ── runner ────────────────────────────────────────────────────────────────────
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
|
adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|
||||||
|
# ── worker (BullMQ — needs full source + tsx) ─────────────────────────────────
|
||||||
|
FROM base AS worker
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["node_modules/.bin/tsx", "jobs/bootstrap.ts"]
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: curio
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./docker/init.sql:/docker-entrypoint-initdb.d/01_init.sql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: worker
|
||||||
|
command: ["node_modules/.bin/drizzle-kit", "migrate"]
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: on-failure
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runner
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
REDIS_URL: ${REDIS_URL}
|
||||||
|
AUTH_SECRET: ${AUTH_SECRET}
|
||||||
|
AUTH_URL: ${AUTH_URL}
|
||||||
|
AUTH_GITHUB_ID: ${AUTH_GITHUB_ID:-}
|
||||||
|
AUTH_GITHUB_SECRET: ${AUTH_GITHUB_SECRET:-}
|
||||||
|
AUTH_GOOGLE_ID: ${AUTH_GOOGLE_ID:-}
|
||||||
|
AUTH_GOOGLE_SECRET: ${AUTH_GOOGLE_SECRET:-}
|
||||||
|
AUTH_OIDC_ISSUER: ${AUTH_OIDC_ISSUER:-}
|
||||||
|
AUTH_OIDC_CLIENT_ID: ${AUTH_OIDC_CLIENT_ID:-}
|
||||||
|
AUTH_OIDC_CLIENT_SECRET: ${AUTH_OIDC_CLIENT_SECRET:-}
|
||||||
|
AUTH_OIDC_DISPLAY_NAME: ${AUTH_OIDC_DISPLAY_NAME:-}
|
||||||
|
LLM_GENERATOR_PROVIDER: ${LLM_GENERATOR_PROVIDER}
|
||||||
|
LLM_GENERATOR_MODEL: ${LLM_GENERATOR_MODEL}
|
||||||
|
LLM_GRADER_PROVIDER: ${LLM_GRADER_PROVIDER}
|
||||||
|
LLM_GRADER_MODEL: ${LLM_GRADER_MODEL}
|
||||||
|
LLM_CHEAP_GRADER_PROVIDER: ${LLM_CHEAP_GRADER_PROVIDER:-}
|
||||||
|
LLM_CHEAP_GRADER_MODEL: ${LLM_CHEAP_GRADER_MODEL:-}
|
||||||
|
LLM_EMBED_PROVIDER: ${LLM_EMBED_PROVIDER:-}
|
||||||
|
LLM_EMBED_MODEL: ${LLM_EMBED_MODEL:-}
|
||||||
|
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
|
||||||
|
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
|
||||||
|
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
||||||
|
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
|
||||||
|
XAI_API_KEY: ${XAI_API_KEY:-}
|
||||||
|
AZURE_API_KEY: ${AZURE_API_KEY:-}
|
||||||
|
AZURE_OPENAI_RESOURCE_NAME: ${AZURE_OPENAI_RESOURCE_NAME:-}
|
||||||
|
COHERE_API_KEY: ${COHERE_API_KEY:-}
|
||||||
|
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-}
|
||||||
|
LANGFUSE_HOST: ${LANGFUSE_HOST:-}
|
||||||
|
LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY:-}
|
||||||
|
LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY:-}
|
||||||
|
UPSTASH_REDIS_REST_URL: ${UPSTASH_REDIS_REST_URL:-}
|
||||||
|
UPSTASH_REDIS_REST_TOKEN: ${UPSTASH_REDIS_REST_TOKEN:-}
|
||||||
|
SESSION_TOKEN_BUDGET: ${SESSION_TOKEN_BUDGET:-}
|
||||||
|
CONTENT_SAFETY_MODE: ${CONTENT_SAFETY_MODE:-}
|
||||||
|
SMTP_HOST: ${SMTP_HOST:-}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-}
|
||||||
|
SMTP_USER: ${SMTP_USER:-}
|
||||||
|
SMTP_PASS: ${SMTP_PASS:-}
|
||||||
|
EMAIL_FROM: ${EMAIL_FROM:-}
|
||||||
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
||||||
|
AWS_REGION: ${AWS_REGION:-}
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.curio.rule=Host(`${DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.curio.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.curio.tls=true"
|
||||||
|
- "traefik.http.routers.curio.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.curio.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: worker
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
REDIS_URL: ${REDIS_URL}
|
||||||
|
LLM_GENERATOR_PROVIDER: ${LLM_GENERATOR_PROVIDER}
|
||||||
|
LLM_GENERATOR_MODEL: ${LLM_GENERATOR_MODEL}
|
||||||
|
LLM_GRADER_PROVIDER: ${LLM_GRADER_PROVIDER}
|
||||||
|
LLM_GRADER_MODEL: ${LLM_GRADER_MODEL}
|
||||||
|
LLM_CHEAP_GRADER_PROVIDER: ${LLM_CHEAP_GRADER_PROVIDER:-}
|
||||||
|
LLM_CHEAP_GRADER_MODEL: ${LLM_CHEAP_GRADER_MODEL:-}
|
||||||
|
LLM_EMBED_PROVIDER: ${LLM_EMBED_PROVIDER:-}
|
||||||
|
LLM_EMBED_MODEL: ${LLM_EMBED_MODEL:-}
|
||||||
|
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
|
||||||
|
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
|
||||||
|
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
||||||
|
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
|
||||||
|
XAI_API_KEY: ${XAI_API_KEY:-}
|
||||||
|
AZURE_API_KEY: ${AZURE_API_KEY:-}
|
||||||
|
AZURE_OPENAI_RESOURCE_NAME: ${AZURE_OPENAI_RESOURCE_NAME:-}
|
||||||
|
COHERE_API_KEY: ${COHERE_API_KEY:-}
|
||||||
|
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-}
|
||||||
|
LANGFUSE_HOST: ${LANGFUSE_HOST:-}
|
||||||
|
LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY:-}
|
||||||
|
LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY:-}
|
||||||
|
SESSION_TOKEN_BUDGET: ${SESSION_TOKEN_BUDGET:-}
|
||||||
|
CONTENT_SAFETY_MODE: ${CONTENT_SAFETY_MODE:-}
|
||||||
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
||||||
|
AWS_REGION: ${AWS_REGION:-}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
redisdata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external: true
|
||||||
|
name: ${TRAEFIK_NETWORK:-traefik}
|
||||||
@@ -4,6 +4,7 @@ import createNextIntlPlugin from 'next-intl/plugin';
|
|||||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
serverExternalPackages: ['@react-pdf/renderer'],
|
serverExternalPackages: ['@react-pdf/renderer'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user