feat(01-01): Next.js app + Docker Compose stack with network segmentation

- Next.js 15 app with Tailwind CSS v4, standalone output for Docker
- Root layout with de locale, page with Tessera placeholder
- Multi-stage Dockerfile for web with monorepo root context
- Docker Compose with 4 services: traefik, web, api, db
- Three segregated networks: frontend-net, backend-net, data-net (internal)
- Traefik v2.11 reverse proxy routing / to web, /api to api
- API strip prefix middleware for clean routing
- PostgreSQL 16-alpine with health checks and named volume
- Fixed API Dockerfile for pnpm monorepo node_modules structure
- Fixed Next.js standalone path for monorepo (apps/web/server.js)
- Fixed Traefik Docker API version compatibility
- Network segmentation: web NOT on data-net, api NOT on frontend-net
This commit is contained in:
2026-06-18 10:17:21 +02:00
parent 04c78b4bdc
commit 2c12878cb1
12 changed files with 1079 additions and 6 deletions
+9 -6
View File
@@ -16,15 +16,18 @@ COPY packages/shared/ ./packages/shared/
COPY tsconfig.base.json ./ COPY tsconfig.base.json ./
RUN pnpm --filter=@tessera/api build RUN pnpm --filter=@tessera/api build
FROM node:24-alpine AS runner FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nestjs && \ RUN addgroup --system --gid 1001 nestjs && \
adduser --system --uid 1001 nestjs adduser --system --uid 1001 nestjs
COPY --from=builder /app/apps/api/dist ./dist COPY --from=deps /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml ./
COPY --from=builder /app/apps/api/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/apps/api/package.json ./ COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
COPY --from=builder /app/node_modules/.pnpm ./node_modules/.pnpm COPY --from=deps /app/apps/api/package.json ./apps/api/
COPY --from=deps /app/packages/shared/package.json ./packages/shared/
COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/shared/src ./packages/shared/src
USER nestjs USER nestjs
EXPOSE 3001 EXPOSE 3001
CMD ["node", "dist/main.js"] CMD ["node", "apps/api/dist/main.js"]
+29
View File
@@ -0,0 +1,29 @@
FROM node:24-alpine AS base
RUN corepack enable && corepack prepare pnpm@9 --activate
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
COPY packages/shared/package.json ./packages/shared/
RUN pnpm install --frozen-lockfile --filter=@tessera/web...
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/ ./
COPY apps/web/ ./apps/web/
COPY packages/shared/ ./packages/shared/
COPY tsconfig.base.json ./
RUN pnpm --filter=@tessera/web build
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder /app/apps/web/public ./apps/web/public
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "apps/web/server.js"]
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone' as const,
};
export default nextConfig;
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@tessera/web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^15.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"tailwindcss": "^4.0.0",
"@tailwindcss/postcss": "^4.0.0",
"postcss": "^8.0.0",
"typescript": "^5.5.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+21
View File
@@ -0,0 +1,21 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Tessera',
description: 'Modulare Workflow-Plattform',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<body className="antialiased">
{children}
</body>
</html>
);
}
+10
View File
@@ -0,0 +1,10 @@
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">Tessera</h1>
<p className="mt-4 text-lg text-gray-600">
Portal wird geladen...
</p>
</main>
);
}
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"allowJs": true,
"noEmit": true,
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+10
View File
@@ -0,0 +1,10 @@
services:
web:
volumes:
- ./apps/web/src:/app/apps/web/src
command: ["pnpm", "--filter", "@tessera/web", "dev"]
api:
volumes:
- ./apps/api/src:/app/apps/api/src
command: ["pnpm", "--filter", "@tessera/api", "start:dev"]
+90
View File
@@ -0,0 +1,90 @@
services:
traefik:
image: traefik:v2.11
ports:
- "80:80"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
environment:
DOCKER_API_VERSION: "1.40"
networks:
- frontend-net
- backend-net
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
web:
build:
context: .
dockerfile: apps/web/Dockerfile
environment:
HOSTNAME: "0.0.0.0"
networks:
- frontend-net
- backend-net
depends_on:
api:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=PathPrefix(`/`)"
- "traefik.http.services.web.loadbalancer.server.port=3000"
- "traefik.http.routers.web.priority=1"
- "traefik.docker.network=tessera-ctl_frontend-net"
api:
build:
context: .
dockerfile: apps/api/Dockerfile
networks:
- backend-net
- data-net
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql://tessera:tessera_dev@db:5432/tessera}
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=PathPrefix(`/api`)"
- "traefik.http.services.api.loadbalancer.server.port=3001"
- "traefik.http.routers.api.priority=2"
- "traefik.http.middlewares.api-strip.stripprefix.prefixes=/api"
- "traefik.http.routers.api.middlewares=api-strip"
- "traefik.docker.network=tessera-ctl_backend-net"
db:
image: postgres:16-alpine
networks:
- data-net
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: tessera
POSTGRES_PASSWORD: ${DB_PASSWORD:-tessera_dev}
POSTGRES_DB: tessera
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tessera"]
interval: 5s
timeout: 3s
retries: 5
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
data-net:
driver: bridge
internal: true
volumes:
pgdata:
+852
View File
File diff suppressed because it is too large Load Diff