From 1707557bea9168bfb974f819d8d044bb913a6a9f Mon Sep 17 00:00:00 2001 From: LovelessCodes Date: Thu, 12 Jun 2025 23:12:24 +0200 Subject: [PATCH] chore: optimize Docker build with multi-stage process and add standalone Next.js output (#75) --- apps/web/Dockerfile | 77 ++++++++++++++++++++++++++++------------- apps/web/entrypoint.sh | 16 +++++++++ apps/web/next.config.js | 1 + 3 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 apps/web/entrypoint.sh diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index ecdfe2c7..ba639153 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -2,37 +2,32 @@ ARG NODE_VERSION=20 ARG APP_DIRNAME=web ARG PROJECT=@kan/web -# 1. Alpine image +# 1. Alpine image with build tools FROM node:${NODE_VERSION}-alpine AS alpine -RUN apk update -RUN apk add --no-cache libc6-compat python3 make g++ +RUN apk update && \ + apk add --no-cache libc6-compat python3 make g++ # Setup pnpm and turbo on the alpine base FROM alpine AS base -RUN corepack enable -# Replace with the major version installed in your repository. For example: -# RUN npm install turbo@2.1.3 --global -RUN npm install turbo@2.3.1 --global - -RUN pnpm config set store-dir ~/.pnpm-store +RUN corepack enable && \ + npm install turbo@2.3.1 dotenv-cli --global && \ + pnpm config set store-dir ~/.pnpm-store +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 # 2. Prune projects FROM base AS pruner -# https://stackoverflow.com/questions/49681984/how-to-get-version-value-of-package-json-inside-of-dockerfile -# RUN export VERSION=$(npm run version) - ARG PROJECT +ARG APP_DIRNAME -# Set working directory WORKDIR /app -# It might be the path to turborepo +# Copy only necessary files for pruning to reduce context size COPY . . - + # Generate a partial monorepo with a pruned lockfile for a target workspace. -# Assuming "@acme/nextjs" is the name entered in the project's package.json: { name: "@acme/nextjs" } +# Include @kan/db for the migration command later. RUN turbo prune --scope=${PROJECT} --scope=@kan/db --docker - + # 3. Build the project FROM base AS builder ARG PROJECT @@ -48,17 +43,35 @@ COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --from=pruner /app/out/json/ . # First install the dependencies (as they change less often) +# Use a cache for pnpm store for faster builds on subsequent runs RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile - + # Copy source code of isolated subworkspace COPY --from=pruner /app/out/full/ . +# Build only the web application RUN pnpm build --filter=${PROJECT} -# 4. Final image - runner stage to run the application +# 4. Production pruner +FROM builder AS productionpruner + +WORKDIR /app + +# Generate a partial monorepo with a pruned lockfile for a target workspace. +# Include @kan/db for the migration command later. +RUN turbo prune --scope=@kan/db --docker + +# 5. Productionn db builder +FROM productionpruner AS productiondbbuilder + +WORKDIR /app/out/full + +RUN pnpm install + +# 6. Final image - runner stage to run the application FROM base AS runner ARG APP_DIRNAME - + # Don't run production as root RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs @@ -68,13 +81,29 @@ WORKDIR /app ENV NODE_ENV=production -COPY --chown=nextjs:nodejs --from=builder /app/ ./ +# Copy only the necessary files for the Next.js application to run +# This includes .next/, public/, and package.json from the web app +# And the shared packages/node_modules that the web app depends on +COPY --chown=nextjs:nodejs --from=builder /app/apps/${APP_DIRNAME}/.next/ ./apps/${APP_DIRNAME}/.next/ +COPY --chown=nextjs:nodejs --from=builder /app/apps/${APP_DIRNAME}/public/ ./apps/${APP_DIRNAME}/public/ +COPY --chown=nextjs:nodejs --from=builder /app/apps/${APP_DIRNAME}/package.json ./apps/${APP_DIRNAME}/package.json +COPY --chown=nextjs:nodejs ./apps/${APP_DIRNAME}/entrypoint.sh /app/apps/${APP_DIRNAME}/entrypoint.sh + +RUN chmod +x /app/apps/${APP_DIRNAME}/entrypoint.sh + +# Copy all node_modules from the production-pruner stage. This is crucial for only having +# the necessary modules for the runner. +COPY --chown=nextjs:nodejs --from=productiondbbuilder /app/out/full/node_modules/ ./node_modules/ +COPY --chown=nextjs:nodejs --from=productiondbbuilder /app/out/full/package.json ./package.json + + +# Copy the db package +COPY --chown=nextjs:nodejs --from=productionpruner /app/out/full/packages/db/ ./packages/db/ + WORKDIR /app/apps/${APP_DIRNAME} ARG PORT=3000 ENV PORT=${PORT} EXPOSE ${PORT} -# Run migration on start for now (until we have a better way to handle this) -CMD ["sh", "-c", "cd /app && pnpm db:migrate && cd /app/apps/web && pnpm start"] - +CMD ["./entrypoint.sh"] \ No newline at end of file diff --git a/apps/web/entrypoint.sh b/apps/web/entrypoint.sh new file mode 100644 index 00000000..85557067 --- /dev/null +++ b/apps/web/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Run migrations from the Turborepo root context +echo "Running database migrations..." +# Navigate to the Turborepo root to run the db:migrate command +pnpm --prefix /app db:migrate + +# Check if migration was successful +if [ $? -ne 0 ]; then + echo "\nDatabase migration failed! Exiting." + exit 1 +fi + +echo "\nStarting Next.js application..." +# Start the Next.js application from the web app's directory +exec node .next/standalone/apps/web/server.js \ No newline at end of file diff --git a/apps/web/next.config.js b/apps/web/next.config.js index e909fa3c..08f0f5d2 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -10,6 +10,7 @@ configureRuntimeEnv(); /** @type {import("next").NextConfig} */ const config = { + output: "standalone", reactStrictMode: true, /** Enables hot reloading for local packages without a build step */