This commit is contained in:
2026-01-17 15:44:22 +01:00
parent 29168a9778
commit f64cb71170
7 changed files with 366 additions and 0 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
.next
.git
.DS_Store
.env
*.md
docs
reference
scripts
public/datasheets/*.pdf

View File

@@ -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
'

59
Dockerfile Normal file
View File

@@ -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"]

5
app/health/route.ts Normal file
View File

@@ -0,0 +1,5 @@
export const dynamic = 'force-dynamic';
export async function GET() {
return new Response('OK', { status: 200 });
}

22
docker-compose.yml Normal file
View File

@@ -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

195
docs/PLATFORM.md Normal file
View File

@@ -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.

View File

@@ -4,6 +4,7 @@ const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{