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