From dda760f8f32fa016f91753b0003598b6cafd7428 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 18 Jun 2025 22:13:57 +0100 Subject: [PATCH] feat: reduce docker image size by using next standalone output --- apps/web/Dockerfile.new | 84 ++++++++++++++++++++++++++++++++++++++++ apps/web/next.config.js | 5 ++- apps/web/src/env.ts | 3 ++ cloud/docker-compose.yml | 62 +++++++++++++++++++++++++++++ docker-compose-old.yml | 81 -------------------------------------- docker-compose.yml | 2 +- turbo.json | 1 + 7 files changed, 155 insertions(+), 83 deletions(-) create mode 100644 apps/web/Dockerfile.new create mode 100644 cloud/docker-compose.yml delete mode 100644 docker-compose-old.yml diff --git a/apps/web/Dockerfile.new b/apps/web/Dockerfile.new new file mode 100644 index 00000000..e6285eff --- /dev/null +++ b/apps/web/Dockerfile.new @@ -0,0 +1,84 @@ +ARG NODE_VERSION=20 +ARG APP_DIRNAME=web +ARG PROJECT=@kan/web + +# 1. Alpine image +FROM node:${NODE_VERSION}-alpine AS alpine +RUN apk update +RUN 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 + +# 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 + +# Set working directory +WORKDIR /app + +# It might be the path to turborepo +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" } +RUN turbo prune --scope=${PROJECT} --scope=@kan/db --docker + +# 3. Build the project +FROM base AS builder +ARG PROJECT + +# Environment to skip .env validation on build +ENV CI=true + +WORKDIR /app + +# Copy lockfile and package.json's of isolated subworkspace +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +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) +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/ . + + +RUN pnpm build --filter=${PROJECT} + +# Copy static files to standalone directory +RUN mkdir -p apps/web/.next/standalone/.next && \ +mv apps/web/public apps/web/.next/standalone/ && \ +mv apps/web/.next/static apps/web/.next/standalone/.next/ + +# 4. 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 +USER nextjs + +WORKDIR /app + +ENV NODE_ENV=production + +COPY --chown=nextjs:nodejs --from=builder /app/ ./ +WORKDIR /app/apps/${APP_DIRNAME} + +ARG PORT=3000 +ENV PORT=${PORT} +EXPOSE ${PORT} + +CMD ["/bin/sh", "./entrypoint.sh"] \ No newline at end of file diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 51c0cb18..2be1af63 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -10,7 +10,10 @@ configureRuntimeEnv(); /** @type {import("next").NextConfig} */ const config = { - // output: "standalone", + output: + env("NEXT_PUBLIC_USE_STANDALONE_OUTPUT") === "true" + ? "standalone" + : "export", reactStrictMode: true, /** Enables hot reloading for local packages without a build step */ diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 194b99c3..3e4cb490 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -81,6 +81,7 @@ export const env = createEnv({ NEXT_PUBLIC_UMAMI_ID: z.string().optional(), NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), + NEXT_PUBLIC_USE_STANDALONE_OUTPUT: z.string().optional(), NEXT_PUBLIC_BASE_URL: z.string().url(), NEXT_PUBLIC_STORAGE_URL: z.string().url().optional(), NEXT_PUBLIC_AVATAR_BUCKET_NAME: z.string().optional(), @@ -115,6 +116,8 @@ export const env = createEnv({ NEXT_PUBLIC_STORAGE_DOMAIN: process.env.NEXT_PUBLIC_STORAGE_DOMAIN, NEXT_PUBLIC_ALLOW_CREDENTIALS: process.env.NEXT_PUBLIC_ALLOW_CREDENTIALS, NEXT_PUBLIC_DISABLE_SIGN_UP: process.env.NEXT_PUBLIC_DISABLE_SIGN_UP, + NEXT_PUBLIC_USE_STANDALONE_OUTPUT: + process.env.NEXT_PUBLIC_USE_STANDALONE_OUTPUT, }, skipValidation: !!process.env.CI || process.env.npm_lifecycle_event === "lint", diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml new file mode 100644 index 00000000..3182d6dc --- /dev/null +++ b/cloud/docker-compose.yml @@ -0,0 +1,62 @@ +services: + web: + image: ghcr.io/kanbn/kan:latest + container_name: ${CONTAINER_NAME:-kan-web} + ports: + - "${WEB_PORT:-3000}:3000" + networks: + - dokploy-network + build: + context: . + dockerfile: ./apps/web/Dockerfile + env_file: + - .env + environment: + - NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV} + - NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - POSTGRES_URL=${POSTGRES_URL} + - NEXT_PUBLIC_USE_STANDALONE_OUTPUT=${NEXT_PUBLIC_USE_STANDALONE_OUTPUT} + + # Stripe + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + + # SMTP + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - EMAIL_FROM=${EMAIL_FROM} + + # S3 storage + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + - S3_REGION=${S3_REGION} + - S3_ENDPOINT=${S3_ENDPOINT} + - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} + - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} + - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} + + # Auth config + - NEXT_PUBLIC_ALLOW_CREDENTIALS=${NEXT_PUBLIC_ALLOW_CREDENTIALS} + - NEXT_PUBLIC_DISABLE_SIGN_UP=${NEXT_PUBLIC_DISABLE_SIGN_UP} + + # Integration providers + - TRELLO_APP_API_KEY=${TRELLO_APP_API_KEY} + - TRELLO_APP_SECRET=${TRELLO_APP_SECRET} + + # OAuth providers (optional) + - BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + + # Analytics + - NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID} + - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} + - NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST} + +networks: + dokploy-network: + external: true +#bump diff --git a/docker-compose-old.yml b/docker-compose-old.yml deleted file mode 100644 index 91e6664b..00000000 --- a/docker-compose-old.yml +++ /dev/null @@ -1,81 +0,0 @@ -services: - web: - image: ghcr.io/kanbn/kan:latest - ports: - - "3001:3000" - networks: - - dokploy-network - build: - context: . - dockerfile: ./apps/web/Dockerfile - env_file: - - .env - command: ["pnpm", "start"] - environment: - - EMAIL_FROM=${EMAIL_FROM} - - SMTP_HOST=${SMTP_HOST} - - SMTP_PORT=${SMTP_PORT} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - - BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} - - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - - POSTGRES_URL=${POSTGRES_URL} - - TRELLO_APP_API_KEY=${TRELLO_APP_API_KEY} - - TRELLO_APP_SECRET=${TRELLO_APP_SECRET} - - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - - DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID} - - DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET} - - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} - - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} - - GITLAB_CLIENT_ID=${GITLAB_CLIENT_ID} - - GITLAB_CLIENT_SECRET=${GITLAB_CLIENT_SECRET} - - GITLAB_ISSUER=${GITLAB_ISSUER} - - MICROSOFT_CLIENT_ID=${MICROSOFT_CLIENT_ID} - - MICROSOFT_CLIENT_SECRET=${MICROSOFT_CLIENT_SECRET} - - TWITTER_CLIENT_ID=${TWITTER_CLIENT_ID} - - TWITTER_CLIENT_SECRET=${TWITTER_CLIENT_SECRET} - - KICK_CLIENT_ID=${KICK_CLIENT_ID} - - KICK_CLIENT_SECRET=${KICK_CLIENT_SECRET} - - ZOOM_CLIENT_ID=${ZOOM_CLIENT_ID} - - ZOOM_CLIENT_SECRET=${ZOOM_CLIENT_SECRET} - - DROPBOX_CLIENT_ID=${DROPBOX_CLIENT_ID} - - DROPBOX_CLIENT_SECRET=${DROPBOX_CLIENT_SECRET} - - VK_CLIENT_ID=${VK_CLIENT_ID} - - VK_CLIENT_SECRET=${VK_CLIENT_SECRET} - - LINKEDIN_CLIENT_ID=${LINKEDIN_CLIENT_ID} - - LINKEDIN_CLIENT_SECRET=${LINKEDIN_CLIENT_SECRET} - - REDDIT_CLIENT_ID=${REDDIT_CLIENT_ID} - - REDDIT_CLIENT_SECRET=${REDDIT_CLIENT_SECRET} - - ROBLOX_CLIENT_ID=${ROBLOX_CLIENT_ID} - - ROBLOX_CLIENT_SECRET=${ROBLOX_CLIENT_SECRET} - - SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID} - - SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET} - - TIKTOK_CLIENT_ID=${TIKTOK_CLIENT_ID} - - TIKTOK_CLIENT_SECRET=${TIKTOK_CLIENT_SECRET} - - TIKTOK_CLIENT_KEY=${TIKTOK_CLIENT_KEY} - - TWITCH_CLIENT_ID=${TWITCH_CLIENT_ID} - - TWITCH_CLIENT_SECRET=${TWITCH_CLIENT_SECRET} - - APPLE_CLIENT_ID=${APPLE_CLIENT_ID} - - APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET} - - APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER} - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_REGION=${S3_REGION} - - S3_ENDPOINT=${S3_ENDPOINT} - - NEXT_PUBLIC_KAN_ENV=${NEXT_PUBLIC_KAN_ENV} - - NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL} - - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} - - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} - - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} - - NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID} - - NEXT_PUBLIC_ALLOW_CREDENTIALS=${NEXT_PUBLIC_ALLOW_CREDENTIALS} - - NEXT_PUBLIC_DISABLE_SIGN_UP=${NEXT_PUBLIC_DISABLE_SIGN_UP} - - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} - - NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST} -networks: - dokploy-network: - external: true -#bump diff --git a/docker-compose.yml b/docker-compose.yml index 927ac68b..deaca027 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - dokploy-network build: context: . - dockerfile: ./apps/web/Dockerfile + dockerfile: ./apps/web/Dockerfile.new env_file: - .env environment: diff --git a/turbo.json b/turbo.json index 31b6e9f6..f6df9e37 100644 --- a/turbo.json +++ b/turbo.json @@ -100,6 +100,7 @@ "NEXT_PUBLIC_AVATAR_BUCKET_NAME", "NEXT_PUBLIC_ALLOW_CREDENTIALS", "NEXT_PUBLIC_DISABLE_SIGN_UP", + "NEXT_PUBLIC_USE_STANDALONE_OUTPUT", "S3_REGION", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY",