feat(deploy): staging/production Docker deploy for Payload (MITHOME-97)
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Canceled after 0s
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Canceled after 0s
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Canceled after 0s
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Canceled after 0s
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Canceled after 0s
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Canceled after 0s
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Canceled after 0s
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Canceled after 0s

Fixes a real, previously-undiscovered build failure and a bigger
architectural gap found while testing an actual `docker build` of
proto/Dockerfile for the first time since the Payload migration:

1. Docker build failure: the (frontend)/[locale] pages use
   generateStaticParams, so `next build` fully prerenders them (SSG) —
   which calls the Payload Local API during the build. With no
   MONGODB_URI/PAYLOAD_SECRET reachable in the build stage, `docker
   build` failed outright ("missing secret key").

2. Bigger problem underneath: even if the build could reach a DB,
   full SSG means a Payload admin edit would NOT appear on the public
   site until a full rebuild + redeploy — directly undermining the
   project's whole reason for migrating to Payload (self-service
   content editing for the client).

Fix (user-confirmed direction: force-dynamic): dropped
generateStaticParams from (frontend)/[locale]/layout.tsx and
[locale]/[slug]/page.tsx, added `export const dynamic = 'force-dynamic'`
to layout.tsx + both page.tsx files. Every request now reads Payload
live — publishing in the admin is visible immediately, and the Docker
build no longer needs any DB connectivity at all (verified: a full
`docker build --target builder` now succeeds with zero env vars set).

Docker/Compose changes:
- docker-compose.staging.yml / docker-compose.prod.yml: wired
  PAYLOAD_SECRET through to the app container (was documented in
  .env.*.example since MITHOME-86 but never actually passed to the
  container — Payload would have refused to start). No fallback,
  same fail-loudly pattern as MONGODB_URI.
- Same two files: added a named `media_data_{staging,prod}` volume
  mounted at /app/media — Payload's local upload storage (Media.ts)
  writes there at the container's runtime cwd; without a volume,
  `deploy.sh`'s `--force-recreate` would silently wipe every uploaded
  logo/image on each deploy.
- docker-compose.dev.yml: was missing PAYLOAD_SECRET entirely (only
  discovered because the same "missing secret key" error reproduces
  there too) — added a dev-only literal value. Media persistence
  already works there via the existing `./proto:/app` bind mount, no
  volume needed. Also dropped the obsolete `version: '3.8'` key
  (compose warns it's ignored).
- DOCKER.md: one-paragraph note on the new PAYLOAD_SECRET requirement
  and where the admin account gets created.

Verified:
- `docker build --target builder` succeeds from a clean context with
  zero environment variables (previously failed).
- `docker build --target runner` + `docker run` against the real dev
  MongoDB (PAYLOAD_SECRET + MONGODB_URI supplied at runtime only):
  /hu, /admin and /api/health all return 200 inside the container;
  confirmed live in the browser that Payload content renders
  correctly end-to-end through the production Next.js server, not
  just `next dev`.
- `docker compose -f docker-compose.{staging,prod,dev}.yml config`
  parses cleanly.
- Full gate green: tsc, lint, proto unit tests (51 passed),
  scripts/pre-deploy-tests.sh (proto tests, tsc, lint, content schema,
  plane-sync — all pass).

deploy.sh itself needs no changes: it already just runs
`docker compose up --build`, and that now works without any
build-time DB wiring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Do Siki
2026-09-11 22:12:01 +02:00
co-authored by Claude Sonnet 5
parent 19622d9703
commit 1b3ae07811
7 changed files with 41 additions and 15 deletions
+2
View File
@@ -37,6 +37,8 @@ A Docker stack a következő szolgáltatásokat indítja:
- Username: `admin` - Username: `admin`
- Password: `admin123` - Password: `admin123`
**Payload CMS admin** (http://localhost:8080/admin): nincs előre létrehozott felhasználó — az első betöltéskor a Payload felkínálja az admin fiók létrehozását. A `docker-compose.dev.yml` egy fix, nem titkos `PAYLOAD_SECRET`-et ad át (MITHOME-97) — staging/production környezetben ez kötelezően a saját `.env.<env>` fájlból jön, nincs alapértelmezett érték (a konténer el sem indul nélküle).
## 📊 MongoDB Hozzáférés ## 📊 MongoDB Hozzáférés
### 1. Mongo Express Web UI ### 1. Mongo Express Web UI
+3 -2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
# Next.js Application # Next.js Application
app: app:
@@ -18,6 +16,9 @@ services:
# with "Command find requires authentication" (MITHOME-98). # with "Command find requires authentication" (MITHOME-98).
- MONGODB_URI=mongodb://admin:password123@mongodb:27017/mozdit?authSource=admin - MONGODB_URI=mongodb://admin:password123@mongodb:27017/mozdit?authSource=admin
- MONGODB_DB=mozdit - MONGODB_DB=mozdit
# Dev-only, nem titkos (MITHOME-97) — Payload ez nélkül el sem indul
# ("missing secret key"), és ez a compose fájl eddig nem adta át.
- PAYLOAD_SECRET=dev-only-insecure-secret-do-not-use-in-staging-or-prod
- NEXT_PUBLIC_SITE_URL=http://localhost:8080 - NEXT_PUBLIC_SITE_URL=http://localhost:8080
- NEXT_PUBLIC_COMPANY_NAME=mozdIT Bt. - NEXT_PUBLIC_COMPANY_NAME=mozdIT Bt.
- NEXT_PUBLIC_CONTACT_EMAIL=info@mozdit.hu - NEXT_PUBLIC_CONTACT_EMAIL=info@mozdit.hu
+9
View File
@@ -17,10 +17,17 @@ services:
# unauthenticated default URI would silently break the app — fail loudly instead. # unauthenticated default URI would silently break the app — fail loudly instead.
- MONGODB_URI=${MONGODB_URI} - MONGODB_URI=${MONGODB_URI}
- MONGODB_DB=${MONGODB_DB:-mozdit} - MONGODB_DB=${MONGODB_DB:-mozdit}
# No fallback either (MITHOME-97): Payload refuses to start without a real
# secret ("missing secret key"), which is exactly what we want here.
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://mozdit.hu} - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://mozdit.hu}
- NEXT_PUBLIC_COMPANY_NAME=${NEXT_PUBLIC_COMPANY_NAME:-mozdIT Bt.} - NEXT_PUBLIC_COMPANY_NAME=${NEXT_PUBLIC_COMPANY_NAME:-mozdIT Bt.}
- NEXT_PUBLIC_CONTACT_EMAIL=${NEXT_PUBLIC_CONTACT_EMAIL:-info@mozdit.hu} - NEXT_PUBLIC_CONTACT_EMAIL=${NEXT_PUBLIC_CONTACT_EMAIL:-info@mozdit.hu}
- LOKI_HOST=${LOKI_HOST:-http://loki:3100} - LOKI_HOST=${LOKI_HOST:-http://loki:3100}
volumes:
# Payload helyi upload storage — lásd docker-compose.staging.yml azonos
# kommentjét (MITHOME-97).
- media_data_prod:/app/media
depends_on: depends_on:
- mongodb - mongodb
networks: networks:
@@ -52,6 +59,8 @@ services:
volumes: volumes:
mongodb_data_prod: mongodb_data_prod:
driver: local driver: local
media_data_prod:
driver: local
networks: networks:
mozdit-network: mozdit-network:
+11
View File
@@ -18,11 +18,20 @@ services:
# unauthenticated default URI would silently break the app — fail loudly instead. # unauthenticated default URI would silently break the app — fail loudly instead.
- MONGODB_URI=${MONGODB_URI} - MONGODB_URI=${MONGODB_URI}
- MONGODB_DB=${MONGODB_DB:-mozdit} - MONGODB_DB=${MONGODB_DB:-mozdit}
# No fallback either (MITHOME-97): Payload refuses to start without a real
# secret ("missing secret key"), which is exactly what we want here.
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://stage.mozdit.hu} - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://stage.mozdit.hu}
- NEXT_PUBLIC_DEPLOY_ENV=staging - NEXT_PUBLIC_DEPLOY_ENV=staging
- NEXT_PUBLIC_COMPANY_NAME=${NEXT_PUBLIC_COMPANY_NAME:-mozdIT Bt.} - NEXT_PUBLIC_COMPANY_NAME=${NEXT_PUBLIC_COMPANY_NAME:-mozdIT Bt.}
- NEXT_PUBLIC_CONTACT_EMAIL=${NEXT_PUBLIC_CONTACT_EMAIL:-info@mozdit.hu} - NEXT_PUBLIC_CONTACT_EMAIL=${NEXT_PUBLIC_CONTACT_EMAIL:-info@mozdit.hu}
- LOKI_HOST=${LOKI_HOST:-http://loki:3100} - LOKI_HOST=${LOKI_HOST:-http://loki:3100}
volumes:
# Payload helyi upload storage (proto/src/collections/Media.ts) — futásidőben
# a konténer WORKDIR-je (/app) alatti media/ mappába ír. Named volume nélkül
# a `deploy.sh` minden újratelepítéskor (--force-recreate) elveszítené a
# korábban feltöltött logókat/képeket (MITHOME-97).
- media_data_staging:/app/media
depends_on: depends_on:
- mongodb - mongodb
networks: networks:
@@ -54,6 +63,8 @@ services:
volumes: volumes:
mongodb_data_staging: mongodb_data_staging:
driver: local driver: local
media_data_staging:
driver: local
networks: networks:
mozdit-network-staging: mozdit-network-staging:
@@ -4,7 +4,7 @@ import AboutView from '@/components/views/AboutView'
import ServicesView from '@/components/views/ServicesView' import ServicesView from '@/components/views/ServicesView'
import ContactView from '@/components/views/ContactView' import ContactView from '@/components/views/ContactView'
import LegalPageView from '@/components/views/LegalPageView' import LegalPageView from '@/components/views/LegalPageView'
import { LOCALES, PAGE_SLUGS, resolvePageKey, isLocale, type Locale, type PageKey } from '@/lib/i18n' import { resolvePageKey, isLocale, type Locale } from '@/lib/i18n'
import { import {
getAboutContent, getAboutContent,
getServicesContent, getServicesContent,
@@ -17,14 +17,9 @@ import { siteConfig, getOgLocale } from '@/config/site'
type Params = { locale: string; slug: string } type Params = { locale: string; slug: string }
export function generateStaticParams() { // WHY force-dynamic: lásd ../layout.tsx — nincs generateStaticParams, minden
return LOCALES.flatMap((locale) => // kérés élőben olvassa a Payload-ot, admin publikálás azonnal látszik.
(Object.keys(PAGE_SLUGS) as PageKey[]).map((key) => ({ export const dynamic = 'force-dynamic'
locale,
slug: PAGE_SLUGS[key][locale],
}))
)
}
const LEGAL_META: Record<'privacy' | 'terms', Record<Locale, string>> = { const LEGAL_META: Record<'privacy' | 'terms', Record<Locale, string>> = {
privacy: { privacy: {
+9 -4
View File
@@ -1,13 +1,18 @@
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import Header from '../../../components/Header' import Header from '../../../components/Header'
import Footer from '../../../components/Footer' import Footer from '../../../components/Footer'
import { LOCALES, isLocale, localePath, type Locale } from '@/lib/i18n' import { isLocale, localePath, type Locale } from '@/lib/i18n'
import { getCommonContent, getHomeContent } from '@/lib/payload-content' import { getCommonContent, getHomeContent } from '@/lib/payload-content'
import { siteConfig, getMainNavigation, getFooterNavigation, getFooterLegalLinks, getSiteDescription } from '@/config/site' import { siteConfig, getMainNavigation, getFooterNavigation, getFooterLegalLinks, getSiteDescription } from '@/config/site'
export function generateStaticParams() { // WHY force-dynamic (MITHOME-97): ezek az oldalak a Payload Local API-t hívják
return LOCALES.map((locale) => ({ locale })) // (élő MongoDB-olvasás). generateStaticParams + SSG mellett a build időben
} // sütött ki minden oldal, és a Payload adminban végzett publikálás csak egy
// teljes redeploy után jelent volna meg a publikus oldalon — ez pont az
// ellentéte az önkiszolgáló szerkesztés céljának, amiért a Payload-migráció
// történt. force-dynamic-kal minden kérés friss Payload-olvasást kap, és a
// Docker build sem függ többé egy build-idejű MongoDB-kapcsolattól.
export const dynamic = 'force-dynamic'
export default async function LocaleLayout({ export default async function LocaleLayout({
children, children,
@@ -7,6 +7,9 @@ import { notFound } from 'next/navigation'
type Params = { locale: string } type Params = { locale: string }
// WHY force-dynamic: lásd [slug]/page.tsx és ../layout.tsx.
export const dynamic = 'force-dynamic'
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> { export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
const { locale: rawLocale } = await params const { locale: rawLocale } = await params
if (!isLocale(rawLocale)) return {} if (!isLocale(rawLocale)) return {}