From a8bc039c026072f5a1d7ede33ef6c0920daea490 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 3 Feb 2026 11:50:17 +0100 Subject: [PATCH] feat: implement centralized Docker base-image strategy and automate registry pushes --- .gitea/workflows/pipeline.yml | 38 ++++++++ apps/sample-website/Dockerfile | 44 +++------- apps/sample-website/docker-compose.yml | 2 +- packages/cli/src/index.ts | 79 +++++++++++------ packages/infra/docker/Dockerfile.app-template | 46 ++++++++++ packages/infra/docker/Dockerfile.gatekeeper | 49 +++++------ packages/infra/docker/Dockerfile.nextjs | 88 ++++--------------- 7 files changed, 187 insertions(+), 159 deletions(-) create mode 100644 packages/infra/docker/Dockerfile.app-template diff --git a/.gitea/workflows/pipeline.yml b/.gitea/workflows/pipeline.yml index 5b335da..e824b6b 100644 --- a/.gitea/workflows/pipeline.yml +++ b/.gitea/workflows/pipeline.yml @@ -80,3 +80,41 @@ jobs: echo "🏷️ Tag detected [${{ github.ref_name }}], performing sync release..." pnpm sync-versions pnpm release:tag + + build-images: + name: 🐳 Build & Push Images + needs: qa + if: startsWith(github.ref, 'refs/tags/v') + runs-on: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: 🐳 Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: 🔐 Registry Login + run: | + echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin + + - name: 🏗️ Build & Push Nextjs Base + env: + TAG: ${{ github.ref_name }} + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t registry.infra.mintel.me/mintel/nextjs:$TAG \ + -t registry.infra.mintel.me/mintel/nextjs:latest \ + -f packages/infra/docker/Dockerfile.nextjs \ + --push . + + - name: 🏗️ Build & Push Gatekeeper + env: + TAG: ${{ github.ref_name }} + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t registry.infra.mintel.me/mintel/gatekeeper:$TAG \ + -t registry.infra.mintel.me/mintel/gatekeeper:latest \ + -f packages/infra/docker/Dockerfile.gatekeeper \ + --push . diff --git a/apps/sample-website/Dockerfile b/apps/sample-website/Dockerfile index 31fe010..9ebbb21 100644 --- a/apps/sample-website/Dockerfile +++ b/apps/sample-website/Dockerfile @@ -1,25 +1,8 @@ -FROM node:20-alpine AS base +# Start from the pre-built Nextjs Base image +FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat curl WORKDIR /app -# Install dependencies based on the preferred package manager -COPY package.json package-lock.json* pnpm-lock.yaml* ./ -RUN corepack enable pnpm && \ - pnpm config set store-dir /root/.local/share/pnpm/store/v3 && \ - pnpm i --frozen-lockfile - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Next.js collects completely anonymous telemetry data about general usage. -ENV NEXT_TELEMETRY_DISABLED=1 - # Build-time environment variables for Next.js ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID @@ -33,11 +16,11 @@ ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET ENV DIRECTUS_URL=$DIRECTUS_URL -# Build the application -RUN corepack enable pnpm && pnpm run build +# Build the specific application +RUN pnpm --filter sample-website build -# Production image, copy all the files and run next -FROM base AS runner +# Production runner image +FROM node:20-alpine AS runner WORKDIR /app # Install curl for health checks @@ -49,15 +32,15 @@ ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=builder /app/public ./public +COPY --from=builder /app/apps/sample-website/public ./apps/sample-website/public # Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next +RUN mkdir -p apps/sample-website/.next +RUN chown nextjs:nodejs apps/sample-website/.next -# Automatically leverage output traces to reduce image size -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# Copy standalone output and static files from the monorepo path +COPY --from=builder --chown=nextjs:nodejs /app/apps/sample-website/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/sample-website/.next/static ./apps/sample-website/.next/static USER nextjs @@ -66,4 +49,5 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +# server.js in monorepo standalone is created for each app +CMD ["node", "apps/sample-website/server.js"] diff --git a/apps/sample-website/docker-compose.yml b/apps/sample-website/docker-compose.yml index 5e2a9de..0273031 100644 --- a/apps/sample-website/docker-compose.yml +++ b/apps/sample-website/docker-compose.yml @@ -9,7 +9,7 @@ services: NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL} NEXT_PUBLIC_TARGET: ${TARGET:-development} DIRECTUS_URL: ${DIRECTUS_URL:-http://directus:8055} - image: sample-website:latest + image: registry.infra.mintel.me/mintel/sample-website:latest container_name: sample-website-app restart: always networks: diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a353b78..45bf521 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -318,34 +318,56 @@ export default function Home() { // Copy infra templates const infraPath = path.resolve(__dirname, "../../infra"); if (await fs.pathExists(infraPath)) { - await fs.copy( - path.join(infraPath, "docker/Dockerfile.nextjs"), - path.join(fullPath, "Dockerfile"), + // Setup Dockerfile from template + const templatePath = path.join( + infraPath, + "docker/Dockerfile.app-template", ); - await fs.copy( - path.join(infraPath, "docker/docker-compose.template.yml"), - path.join(fullPath, "docker-compose.yml"), + if (await fs.pathExists(templatePath)) { + let dockerfile = await fs.readFile(templatePath, "utf8"); + dockerfile = dockerfile.replace(/\$\{APP_NAME:-app\}/g, projectName); + await fs.writeFile(path.join(fullPath, "Dockerfile"), dockerfile); + } + + // Setup docker-compose from template + const composeTemplatePath = path.join( + infraPath, + "docker/docker-compose.template.yml", ); + if (await fs.pathExists(composeTemplatePath)) { + let compose = await fs.readFile(composeTemplatePath, "utf8"); + compose = compose.replace(/\$\{APP_NAME:-app\}/g, projectName); + compose = compose.replace(/\$\{PROJECT_NAME:-app\}/g, projectName); + await fs.writeFile( + path.join(fullPath, "docker-compose.yml"), + compose, + ); + } + await fs.ensureDir(path.join(fullPath, ".gitea/workflows")); - await fs.copy( - path.join(infraPath, "gitea/deploy-action.yml"), - path.join(fullPath, ".gitea/workflows/deploy.yml"), + const deployActionPath = path.join( + infraPath, + "gitea/deploy-action.yml", ); + if (await fs.pathExists(deployActionPath)) { + await fs.copy( + deployActionPath, + path.join(fullPath, ".gitea/workflows/deploy.yml"), + ); + } + } - // Create Directus structure - await fs.ensureDir(path.join(fullPath, "directus/uploads")); - await fs.ensureDir(path.join(fullPath, "directus/extensions")); - await fs.writeFile( - path.join(fullPath, "directus/uploads/.gitkeep"), - "", - ); - await fs.writeFile( - path.join(fullPath, "directus/extensions/.gitkeep"), - "", - ); + // Create Directus structure + await fs.ensureDir(path.join(fullPath, "directus/uploads")); + await fs.ensureDir(path.join(fullPath, "directus/extensions")); + await fs.writeFile(path.join(fullPath, "directus/uploads/.gitkeep"), ""); + await fs.writeFile( + path.join(fullPath, "directus/extensions/.gitkeep"), + "", + ); - // Create .env.example - const envExample = `# Project + // Create .env.example + const envExample = `# Project PROJECT_NAME=${projectName} PROJECT_COLOR=#82ed20 @@ -377,14 +399,13 @@ SENTRY_DSN= NEXT_PUBLIC_UMAMI_WEBSITE_ID= NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js `; - await fs.writeFile(path.join(fullPath, ".env.example"), envExample); + await fs.writeFile(path.join(fullPath, ".env.example"), envExample); - // Copy premium templates (globals.css, lib/directus.ts, scripts/setup-directus.ts) - const templatePath = path.join(infraPath, "templates/website"); - if (await fs.pathExists(templatePath)) { - console.log(chalk.blue("Applying premium templates...")); - await fs.copy(templatePath, fullPath, { overwrite: true }); - } + // Copy premium templates (globals.css, lib/directus.ts, scripts/setup-directus.ts) + const templatePath = path.join(infraPath, "templates/website"); + if (await fs.pathExists(templatePath)) { + console.log(chalk.blue("Applying premium templates...")); + await fs.copy(templatePath, fullPath, { overwrite: true }); } console.log( diff --git a/packages/infra/docker/Dockerfile.app-template b/packages/infra/docker/Dockerfile.app-template new file mode 100644 index 0000000..2ce85b6 --- /dev/null +++ b/packages/infra/docker/Dockerfile.app-template @@ -0,0 +1,46 @@ +# Start from the pre-built Nextjs Base image +FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder + +WORKDIR /app + +# Build-time environment variables for Next.js +ARG NEXT_PUBLIC_BASE_URL +ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID +ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL +ARG NEXT_PUBLIC_TARGET +ARG DIRECTUS_URL + +ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL +ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID +ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL +ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET +ENV DIRECTUS_URL=$DIRECTUS_URL + +# Build the specific application +RUN pnpm --filter ${APP_NAME:-app} build + +# Production runner image +FROM node:20-alpine AS runner +WORKDIR /app + +# Install curl for health checks +RUN apk add --no-cache curl + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy standalone output and static files +# Note: The path depends on the app name +COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME:-app}/public ./apps/${APP_NAME:-app}/public +COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME:-app}/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME:-app}/.next/static ./apps/${APP_NAME:-app}/.next/static + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "apps/${APP_NAME:-app}/server.js"] diff --git a/packages/infra/docker/Dockerfile.gatekeeper b/packages/infra/docker/Dockerfile.gatekeeper index c006c30..7474fba 100644 --- a/packages/infra/docker/Dockerfile.gatekeeper +++ b/packages/infra/docker/Dockerfile.gatekeeper @@ -1,47 +1,42 @@ FROM node:20-alpine AS base -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat curl WORKDIR /app -# Install dependencies -COPY package.json pnpm-lock.yaml* ./ -RUN corepack enable pnpm && pnpm i --frozen-lockfile +# Enable pnpm +RUN corepack enable pnpm -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules +# Install dependencies (using monorepo root context) +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./ +COPY packages/gatekeeper/package.json ./packages/gatekeeper/ +COPY packages/next-utils/package.json ./packages/next-utils/ +COPY packages/tsconfig/package.json ./packages/tsconfig/ +COPY packages/eslint-config/package.json ./packages/eslint-config/ +COPY packages/next-config/package.json ./packages/next-config/ + +RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \ + pnpm i --frozen-lockfile + +# Copy source COPY . . -ENV NEXT_TELEMETRY_DISABLED=1 +# Build Gatekeeper +RUN pnpm --filter @mintel/gatekeeper build -# Build the application -RUN corepack enable pnpm && pnpm run build - -# Production image, copy all the files and run next +# Runner FROM base AS runner WORKDIR /app - -RUN apk add --no-cache curl - ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder /app/packages/gatekeeper/public ./packages/gatekeeper/public +COPY --from=builder /app/packages/gatekeeper/.next/standalone ./ +COPY --from=builder /app/packages/gatekeeper/.next/static ./packages/gatekeeper/.next/static USER nextjs - EXPOSE 3000 - ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +CMD ["node", "packages/gatekeeper/server.js"] diff --git a/packages/infra/docker/Dockerfile.nextjs b/packages/infra/docker/Dockerfile.nextjs index 76fcadf..4327b0c 100644 --- a/packages/infra/docker/Dockerfile.nextjs +++ b/packages/infra/docker/Dockerfile.nextjs @@ -1,80 +1,24 @@ FROM node:20-alpine AS base -# Install dependencies only when needed -FROM base AS deps RUN apk add --no-cache libc6-compat curl WORKDIR /app -# Install dependencies based on the preferred package manager -COPY package.json package-lock.json* pnpm-lock.yaml* ./ -RUN if [ -f pnpm-lock.yaml ]; then \ - corepack enable pnpm && \ - pnpm config set store-dir /root/.local/share/pnpm/store/v3 && \ - pnpm i --frozen-lockfile; \ - elif [ -f package-lock.json ]; then \ - npm ci; \ - else \ - npm i; \ - fi +# Enable pnpm +RUN corepack enable pnpm -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules +# Copy root configurations +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./ + +# Copy all package.json files to allow pnpm install to be cached +COPY packages/*/package.json ./packages/ +COPY apps/*/package.json ./apps/ + +# Install dependencies for the entire monorepo +RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \ + pnpm i --frozen-lockfile + +# Copy the rest of the source code COPY . . -# Next.js collects completely anonymous telemetry data about general usage. -ENV NEXT_TELEMETRY_DISABLED=1 - -# Build-time environment variables for Next.js -ARG NEXT_PUBLIC_BASE_URL -ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID -ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL -ARG NEXT_PUBLIC_TARGET -ARG DIRECTUS_URL - -ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL -ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID -ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL -ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET -ENV DIRECTUS_URL=$DIRECTUS_URL - -# Build the application -RUN if [ -f pnpm-lock.yaml ]; then \ - corepack enable pnpm && \ - pnpm run build; \ - else \ - npm run build; \ - fi - -# Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - -# Install curl for health checks -RUN apk add --no-cache curl - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public - -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next - -# Automatically leverage output traces to reduce image size -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"] +# Post-install/Build shared packages if needed +RUN pnpm -r build --filter="./packages/*"