diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6c7a7336 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.next +.git +.DS_Store +.env +*.md +docs +reference +scripts +public/datasheets/*.pdf diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 00000000..e1dfe65f --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,74 @@ +name: Build & Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: docker + + steps: + # --- Tools --- + - name: Install tools + run: | + apt-get update + apt-get install -y \ + git \ + docker.io \ + openssh-client \ + rsync + + # --- Checkout --- + - name: Checkout repo + run: | + git clone https://git.infra.mintel.me/mintel/klz-cables.git . + git checkout main + + # --- Docker registry login --- + - name: Login to registry + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }} + run: | + echo "$REGISTRY_PASS" | docker login registry.infra.mintel.me \ + -u "$REGISTRY_USER" \ + --password-stdin + + # --- Build image --- + - name: Build image + run: | + docker build \ + -t registry.infra.mintel.me/mintel/klz-cables:latest . + + # --- Push image --- + - name: Push image + run: | + docker push registry.infra.mintel.me/mintel/klz-cables:latest + + # --- SSH setup --- + - name: Setup SSH + env: + ALPHA_SSH_KEY: ${{ secrets.ALPHA_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$ALPHA_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts + + # --- Sync compose (yml OR yaml) --- + - name: Sync compose file + run: | + rsync -av ./docker-compose.y*ml \ + deploy@alpha.mintel.me:/home/deploy/sites/klz-cables/ + + # --- Deploy --- + - name: Deploy on server + run: | + ssh deploy@alpha.mintel.me ' + cd /home/deploy/sites/klz-cables && + docker compose -f docker-compose.yml pull 2>/dev/null || + docker compose -f docker-compose.yaml pull && + docker compose up -d + ' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..53eea244 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN npm ci + + +# 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. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +# 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 +# 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 + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +# set hostname to localhost +ENV HOSTNAME "0.0.0.0" + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD ["node", "server.js"] diff --git a/app/health/route.ts b/app/health/route.ts new file mode 100644 index 00000000..af7d186f --- /dev/null +++ b/app/health/route.ts @@ -0,0 +1,5 @@ +export const dynamic = 'force-dynamic'; + +export async function GET() { + return new Response('OK', { status: 200 }); +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e501b2b7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + app: + image: registry.infra.mintel.me/mintel/klz-cables:latest + restart: always + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.klz-cables.rule=Host(klz-cables.com,www.klz-cables.com)" + - "traefik.http.routers.klz-cables.entrypoints=websecure" + - "traefik.http.routers.klz-cables.tls.certresolver=le" + - "traefik.http.services.klz-cables.loadbalancer.server.port=3000" + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + +networks: + traefik: + external: true diff --git a/docs/PLATFORM.md b/docs/PLATFORM.md new file mode 100644 index 00000000..0a8c77bf --- /dev/null +++ b/docs/PLATFORM.md @@ -0,0 +1,195 @@ +# Mintel Alpha Platform — Developer Cheat Sheet + +This platform runs real customer websites on their own domains +(e.g. klz-cables.com, marisas.world, shop.customer.de). + +You do not manage servers. +You ship Docker containers. +Mintel runs the platform. + +--- + +## Control Plane (Infra) + +Internal services (developers only): + +Git (Gitea) +https://git.infra.mintel.me + +CI (Gitea Actions) +https://git.infra.mintel.me/actions + +Container Registry +https://registry.infra.mintel.me + +Error Tracking (GlitchTip) +https://errors.infra.mintel.me + +Analytics (Umami) +https://analytics.infra.mintel.me + +Uptime +https://status.infra.mintel.me + +Logs (Dozzle) +https://logs.infra.mintel.me + +--- + +## Production Platform (Alpha) + +Alpha runs all customer websites and is publicly reachable. + +- Listens on ports 80 / 443 +- Runs Traefik +- Routes real domains +- Is isolated from Infra + +Customer DNS A records point to the Alpha server IP. + +--- + +## Routing (Traefik) + +Routing is host-based. + +Each service declares its domains via labels: + +labels: + - traefik.enable=true + - traefik.http.routers.app.rule=Host(example.com,www.example.com) + - traefik.http.routers.app.entrypoints=websecure + - traefik.http.routers.app.tls.certresolver=le + - traefik.http.services.app.loadbalancer.server.port=3000 + +Traefik: +- terminates TLS +- auto-issues certificates +- supports zero-downtime deploys + +--- + +## Directory layout on Alpha + +Each app lives in: + +/opt/alpha/sites/APP_NAME + +Contains: + +docker-compose.yml +.env (optional) +content/ +db/ + +--- + +## Container Images + +All production images are built by CI and pushed to the Mintel Registry. + +Registry: +registry.infra.mintel.me + +Naming: +registry.infra.mintel.me/ORG/APP_NAME:TAG + +Example: +registry.infra.mintel.me/mintel/mb-grid-solutions:latest + +--- + +## Databases + +### Postgres (shared) + +One Postgres server, many databases. + +Connection format: +postgres://infra:infra@postgres:5432/APP_DB + +Each app must use its own database. + +--- + +### Redis (shared) + +One Redis instance, multiple DB indexes. + +redis://redis:6379/1 +redis://redis:6379/2 + +Each app must use its own DB number. + +--- + +## Health checks (required) + +Every public service must expose: + +GET /health → 200 OK when ready + +Used by Traefik for zero-downtime routing. + +--- + +## Error Tracking (GlitchTip) + +Each app gets a DSN: + +https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID + +Set as: +SENTRY_DSN + +--- + +## Analytics (Umami) + +Each site gets a website ID. + +Include: + +https://analytics.infra.mintel.me/script.js +data-website-id=YOUR_ID + +--- + +## Deployment (Gitea Actions) + +Flow: + +- Push to main +- CI builds image +- Image pushed to registry +- Alpha pulls and runs +- Traefik routes traffic + +Deploy target: +deploy@alpha.mintel.me + +--- + +## Monitoring + +Errors → GlitchTip +Traffic → Umami +Logs → Dozzle +Uptime → Uptime-Kuma + +Infra monitors everything. + +--- + +## Summary + +You push code +CI builds images +Registry stores images +Alpha runs containers +Traefik routes domains +Databases are shared but isolated +Deploys are zero-downtime +Everything is monitored + +This is a real production platform. \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index a41735dd..494de2fd 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -4,6 +4,7 @@ const withNextIntl = createNextIntlPlugin(); /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', images: { remotePatterns: [ {