Docker
Die App in ein reproduzierbares Image packen — genau das Setup, mit dem dieser Kurs deployed wird.
Warum Multi-Stage?
Wir bauen in zwei Stufen: Die builder-Stufe installiert alle Abhängigkeiten und
erzeugt build/. Die schlanke runner-Stufe übernimmt nur das
Nötige und startet den Server. Das hält das finale Image klein und sauber.
# ── Stage 1: build (adapter-node → build/) ──────────────────
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Stage 2: production runner ──────────────────────────────
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
COPY entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
EXPOSE 3000
CMD ["./entrypoint.sh"]🛤️ Rails-Vergleich: Dockerfile & db:migrate beim Start
Das Muster kennst du: bauen, dann Server starten. Unser entrypoint.sh führt vor
dem Start die Migrationen aus — genau wie ein bin/rails db:migrate im Release-Step.
# Rails-Dockerfile: sehr ähnliche Idee
FROM ruby:3.3-alpine
# ... bundle install, assets:precompile ...
CMD ["bin/rails", "server", "-b", "0.0.0.0"]
# Migrationen beim Start: bin/rails db:migrate (wie unser entrypoint)Der Entrypoint: erst migrieren, dann starten
#!/bin/sh
set -e
echo "Running migrations…"
node_modules/.bin/drizzle-kit migrate
if [ "$SEED_ON_START" = "true" ]; then node scripts/seed.js; fi
exec node buildSo ist die Datenbank beim ersten Request garantiert auf dem aktuellen Stand. Mit SEED_ON_START=true werden zusätzlich Demo-Daten eingespielt.
Lokal testen mit Compose
docker-compose.yml startet Postgres und die App zusammen — ideal, um das
Produktions-Image vor dem Deploy zu prüfen:
services:
db:
image: postgres:16-alpine
environment: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: devnotes }
app:
build: .
depends_on: [db]
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/devnotes
ORIGIN: http://localhost:3000
ports: ['3000:3000']docker compose up --build # DB + App lokal starten → localhost:3000⚠️ DATABASE_URL zur Laufzeit
Der DB-Client liest die URL über $env/dynamic/private — also zur Laufzeit, nicht beim Build. Dadurch funktioniert dasselbe Image lokal und in
der Cloud; nur die Env-Variable unterscheidet sich.
Zusammenfassung
💡 Das Wichtigste
- Multi-Stage-Build: bauen in Stage 1, schlank ausliefern in Stage 2.
- Der Container hört auf Port 3000 (
HOST=0.0.0.0). - Migrationen laufen im Entrypoint vor dem Serverstart.
🎯 Probier es selbst
Führe docker compose up --build aus und öffne http://localhost:3000. Registriere ein Konto in DevNotes — die Daten landen im
Postgres-Container.