build: Docker-Betrieb und Standalone-Betrieb gleichwertig unterstuetzen
Dieselbe Codebasis laeuft nativ auf dem Rechner oder komplett im Container. docker-compose.yml mit Profilen statt mehrerer Dateien: - "db" laeuft immer -> docker compose up -d db, App dann lokal per npm - --profile full -> zusaetzlich der App-Container - --profile tools -> Adminer als Datenbankoberflaeche Dockerfile als Multi-Stage-Build auf dem Next-Standalone-Output. In die Laufzeitschicht wandern zusaetzlich Prisma-CLI, Engines und Schema, damit der Entrypoint beim Start "migrate deploy" ausfuehren kann. Das Alpine-Image bekommt openssl fuer die Prisma-Engines, der Client wird zusaetzlich fuer linux-musl-openssl-3.0.x erzeugt. Der Container laeuft als unprivilegierter Benutzer und meldet seinen Zustand ueber /api/health. Der Entrypoint wartet in einer Schleife auf die Datenbank, ergaenzend zum pg_isready-Healthcheck und der depends_on-Bedingung. scripts/setup.mjs legt die .env aus der Vorlage an und erzeugt dabei ein zufaelliges APP_SECRET, damit die Tokens nicht mit dem Standardschluessel verschluesselt werden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Nebula – Beispielkonfiguration
|
||||||
|
# Kopieren nach ".env" und Werte anpassen.
|
||||||
|
# (Die .env wird sowohl von Next.js als auch von docker compose gelesen.)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── Datenbank ────────────────────────────────────────────────
|
||||||
|
# Standalone-Betrieb: App laeuft lokal, Postgres via "docker compose up -d db"
|
||||||
|
# oder als nativ installierter Dienst.
|
||||||
|
DATABASE_URL="postgresql://nebula:nebula@localhost:5432/nebula?schema=public"
|
||||||
|
|
||||||
|
# Im Docker-Vollbetrieb (--profile full) setzt compose DATABASE_URL selbst
|
||||||
|
# auf den Service-Hostnamen "db" – der Wert oben wird dann nicht verwendet.
|
||||||
|
|
||||||
|
# Zugangsdaten des Postgres-Containers aus docker-compose.yml
|
||||||
|
POSTGRES_USER="nebula"
|
||||||
|
POSTGRES_PASSWORD="nebula"
|
||||||
|
POSTGRES_DB="nebula"
|
||||||
|
# Host-Port, unter dem die Datenbank erreichbar ist
|
||||||
|
POSTGRES_PORT="5432"
|
||||||
|
|
||||||
|
# ── Sicherheit ───────────────────────────────────────────────
|
||||||
|
# Schluessel zur Verschluesselung der Git-Access-Tokens (AES-256-GCM).
|
||||||
|
# Erzeugen mit: openssl rand -base64 32
|
||||||
|
# oder: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
||||||
|
# oder: npm run setup (legt die .env inkl. Schluessel an)
|
||||||
|
# WICHTIG: Wird der Wert nachtraeglich geaendert, sind gespeicherte Tokens
|
||||||
|
# nicht mehr entschluesselbar und muessen neu hinterlegt werden.
|
||||||
|
APP_SECRET="bitte-aendern-mindestens-32-zeichen-langer-zufallswert"
|
||||||
|
|
||||||
|
# ── App ──────────────────────────────────────────────────────
|
||||||
|
# Port innerhalb des Containers bzw. im Standalone-Betrieb
|
||||||
|
PORT="3000"
|
||||||
|
# Host-Port-Mapping fuer den App-Container (--profile full)
|
||||||
|
APP_PORT="3000"
|
||||||
|
# Host-Port fuer Adminer (--profile tools)
|
||||||
|
ADMINER_PORT="8080"
|
||||||
|
|
||||||
|
# Timeout fuer Git-API-Aufrufe in Millisekunden
|
||||||
|
GIT_API_TIMEOUT_MS="15000"
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Nebula – Produktions-Image (Next.js standalone + Prisma)
|
||||||
|
# Build: docker compose --profile full build
|
||||||
|
# Start: docker compose --profile full up -d
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
FROM node:22-alpine AS base
|
||||||
|
# openssl wird von den Prisma-Engines benoetigt, libc6-compat von Next.js.
|
||||||
|
RUN apk add --no-cache libc6-compat openssl
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
|
||||||
|
# ── Abhaengigkeiten ──────────────────────────────────────────
|
||||||
|
FROM base AS deps
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
# prisma/ wird schon hier gebraucht, weil "postinstall" prisma generate ausfuehrt.
|
||||||
|
COPY prisma ./prisma
|
||||||
|
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
||||||
|
|
||||||
|
|
||||||
|
# ── Build ────────────────────────────────────────────────────
|
||||||
|
FROM base AS builder
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
# Zur Build-Zeit wird keine echte Datenbank kontaktiert (alle Seiten sind
|
||||||
|
# dynamisch), Prisma verlangt die Variable aber beim Generieren.
|
||||||
|
ENV DATABASE_URL="postgresql://build:build@127.0.0.1:5432/build?schema=public"
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# ── Laufzeit ─────────────────────────────────────────────────
|
||||||
|
FROM base AS runner
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# Prisma-CLI, Engines und Schema fuer "migrate deploy" beim Containerstart.
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||||
|
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
||||||
|
CMD wget -q --spider http://127.0.0.1:3000/api/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: nebula
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Zwei Betriebsarten aus einer Datei:
|
||||||
|
#
|
||||||
|
# 1) Standalone-Entwicklung – nur die Datenbank im Container:
|
||||||
|
# docker compose up -d db
|
||||||
|
# npm run dev
|
||||||
|
#
|
||||||
|
# 2) Alles im Container:
|
||||||
|
# docker compose --profile full up -d --build
|
||||||
|
#
|
||||||
|
# Optionales DB-Webinterface:
|
||||||
|
# docker compose --profile tools up -d adminer → http://localhost:8080
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: nebula-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-nebula}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-nebula}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-nebula}
|
||||||
|
# Deterministische Sortierung unabhaengig vom Host-Locale
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- nebula-db-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-nebula} -d ${POSTGRES_DB:-nebula}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
app:
|
||||||
|
profiles: ["full"]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: nebula-app:latest
|
||||||
|
container_name: nebula-app
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-nebula}:${POSTGRES_PASSWORD:-nebula}@db:5432/${POSTGRES_DB:-nebula}?schema=public
|
||||||
|
# Unbedingt in der .env ueberschreiben – verschluesselt die Git-Tokens.
|
||||||
|
APP_SECRET: ${APP_SECRET:-nebula-dev-secret-bitte-in-der-env-aendern}
|
||||||
|
GIT_API_TIMEOUT_MS: ${GIT_API_TIMEOUT_MS:-15000}
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-3000}:3000"
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
profiles: ["tools"]
|
||||||
|
image: adminer:latest
|
||||||
|
container_name: nebula-adminer
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: db
|
||||||
|
ADMINER_DESIGN: dracula
|
||||||
|
ports:
|
||||||
|
- "${ADMINER_PORT:-8080}:8080"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nebula-db-data:
|
||||||
|
name: nebula-db-data
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Startet Nebula im Container: erst Migrationen, dann der Server.
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PRISMA_CLI="/app/node_modules/prisma/build/index.js"
|
||||||
|
SCHEMA="/app/prisma/schema.prisma"
|
||||||
|
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "[nebula] FEHLER: DATABASE_URL ist nicht gesetzt." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[nebula] Migrationen werden eingespielt…"
|
||||||
|
|
||||||
|
attempt=0
|
||||||
|
max_attempts=15
|
||||||
|
until node "$PRISMA_CLI" migrate deploy --schema "$SCHEMA"; do
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||||
|
echo "[nebula] FEHLER: Migration nach $max_attempts Versuchen fehlgeschlagen." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[nebula] Datenbank noch nicht bereit – neuer Versuch in 3s ($attempt/$max_attempts)"
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[nebula] Migrationen ok. Server startet auf Port ${PORT:-3000}."
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Nebula – Ersteinrichtung.
|
||||||
|
*
|
||||||
|
* Legt eine .env auf Basis von .env.example an und erzeugt dabei einen
|
||||||
|
* zufaelligen APP_SECRET. Vorhandene .env-Dateien werden nicht angefasst.
|
||||||
|
*
|
||||||
|
* node scripts/setup.mjs bzw. npm run setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const envPath = join(root, ".env");
|
||||||
|
const examplePath = join(root, ".env.example");
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
reset: "[0m",
|
||||||
|
dim: "[2m",
|
||||||
|
green: "[32m",
|
||||||
|
yellow: "[33m",
|
||||||
|
cyan: "[36m",
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(examplePath)) {
|
||||||
|
console.error(`${c.yellow}.env.example nicht gefunden – abgebrochen.${c.reset}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(envPath)) {
|
||||||
|
log(`${c.yellow}·${c.reset} .env existiert bereits – sie wird nicht ueberschrieben.`);
|
||||||
|
} else {
|
||||||
|
const secret = randomBytes(32).toString("base64");
|
||||||
|
const content = readFileSync(examplePath, "utf8").replace(
|
||||||
|
/^APP_SECRET=.*$/m,
|
||||||
|
`APP_SECRET="${secret}"`,
|
||||||
|
);
|
||||||
|
writeFileSync(envPath, content, "utf8");
|
||||||
|
log(`${c.green}✓${c.reset} .env angelegt (APP_SECRET wurde zufaellig erzeugt).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log("");
|
||||||
|
log(`${c.cyan}Naechste Schritte${c.reset}`);
|
||||||
|
log(`${c.dim}─────────────────────────────────────────────${c.reset}`);
|
||||||
|
log("Standalone (App lokal, Datenbank im Container):");
|
||||||
|
log(` ${c.dim}1.${c.reset} docker compose up -d db`);
|
||||||
|
log(` ${c.dim}2.${c.reset} npm install`);
|
||||||
|
log(` ${c.dim}3.${c.reset} npm run db:migrate`);
|
||||||
|
log(` ${c.dim}4.${c.reset} npm run db:seed ${c.dim}(optional, Beispieldaten)${c.reset}`);
|
||||||
|
log(` ${c.dim}5.${c.reset} npm run dev ${c.dim}→ http://localhost:3000${c.reset}`);
|
||||||
|
log("");
|
||||||
|
log("Alles in Docker:");
|
||||||
|
log(` ${c.dim}1.${c.reset} docker compose --profile full up -d --build`);
|
||||||
|
log(` ${c.dim}2.${c.reset} ${c.dim}→ http://localhost:3000${c.reset}`);
|
||||||
|
log("");
|
||||||
|
log("Eigene, bereits laufende PostgreSQL:");
|
||||||
|
log(` DATABASE_URL in der .env anpassen, dann npm run db:migrate`);
|
||||||
|
log("");
|
||||||
Reference in New Issue
Block a user