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:
+9
-6
@@ -16,15 +16,18 @@ COPY packages/shared/ ./packages/shared/
|
||||
COPY tsconfig.base.json ./
|
||||
RUN pnpm --filter=@tessera/api build
|
||||
|
||||
FROM node:24-alpine AS runner
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN addgroup --system --gid 1001 nestjs && \
|
||||
adduser --system --uid 1001 nestjs
|
||||
COPY --from=builder /app/apps/api/dist ./dist
|
||||
COPY --from=builder /app/apps/api/node_modules ./node_modules
|
||||
COPY --from=builder /app/apps/api/package.json ./
|
||||
COPY --from=builder /app/node_modules/.pnpm ./node_modules/.pnpm
|
||||
COPY --from=deps /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml ./
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||
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
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/main.js"]
|
||||
CMD ["node", "apps/api/dist/main.js"]
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone' as const,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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:
|
||||
Generated
+852
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user