diff --git a/docker-compose.portainer.staging.yml b/docker-compose.portainer.staging.yml index c08ae48..99cc608 100644 --- a/docker-compose.portainer.staging.yml +++ b/docker-compose.portainer.staging.yml @@ -18,6 +18,10 @@ # TYPESENSE_SEARCH_KEY — Typesense search-only key (exposed to frontend) # UNLEASH_URL — http://:4242/api (empty = all flags off) # UNLEASH_API_TOKEN — Unleash *client* token, environment: development +# PAYLOAD_SECRET — Payload CMS encryption secret. REQUIRED: long and +# random, and DIFFERENT from production's. Sharing +# it would let a staging session authenticate +# against production. # AIRFLOW_ADMIN_USER — Airflow admin username (default: admin) # AIRFLOW_ADMIN_PASSWORD — Airflow admin password. REQUIRED: the api-server # refuses to start without it, rather than falling @@ -89,9 +93,20 @@ services: - FASTAPI_URL=http://backend:80/api - TYPESENSE_URL=http://typesense:8108 - TYPESENSE_API_KEY=${TYPESENSE_SEARCH_KEY:-changeme} + # Payload CMS runs inside this container, in the `payload` schema of the + # staging database. Staging has its own stack, its own Postgres and its + # own admin account — never production's. + - DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@sc_database:5432/${DB_DATABASE_NAME} + - PAYLOAD_SECRET=${PAYLOAD_SECRET:?set PAYLOAD_SECRET in the staging Portainer stack environment} + volumes: + # Portainer prefixes volume names with the stack name, so this is + # automatically isolated from production's media. + - payload_media:/app/media depends_on: backend: condition: service_healthy + sc_database: + condition: service_healthy networks: backend: {} macvlan: @@ -242,3 +257,4 @@ volumes: typesense_data: airflow_logs: unleash_cache: + payload_media: diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml index 00e085e..fd2be50 100644 --- a/docker-compose.portainer.yml +++ b/docker-compose.portainer.yml @@ -9,6 +9,9 @@ # TYPESENSE_SEARCH_KEY — Typesense search-only key (exposed to frontend) # UNLEASH_URL — http://:4242/api (empty = all flags off) # UNLEASH_API_TOKEN — Unleash *client* token, environment: production +# PAYLOAD_SECRET — Payload CMS encryption secret. REQUIRED: long and +# random. Changing it invalidates every admin +# session. Staging MUST use a different value. # AIRFLOW_ADMIN_USER — Airflow admin username (default: admin) # AIRFLOW_ADMIN_PASSWORD — Airflow admin password. REQUIRED: the api-server # refuses to start without it, rather than falling @@ -78,9 +81,21 @@ services: - FASTAPI_URL=http://backend:80/api - TYPESENSE_URL=http://typesense:8108 - TYPESENSE_API_KEY=${TYPESENSE_SEARCH_KEY:-changeme} + # Payload CMS runs inside this container. It reaches Postgres over the + # `backend` network and keeps its tables in the `payload` schema, so no + # pipeline operation on `public` can touch blog content. + - DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@sc_database:5432/${DB_DATABASE_NAME} + # Same :? form as AIRFLOW_ADMIN_PASSWORD: refuse to start rather than + # boot with an empty secret and silently accept forged sessions. + - PAYLOAD_SECRET=${PAYLOAD_SECRET:?set PAYLOAD_SECRET in the Portainer stack environment} + volumes: + # Blog images. Not reproducible from the pipeline — must be backed up. + - payload_media:/app/media depends_on: backend: condition: service_healthy + sc_database: + condition: service_healthy networks: backend: {} macvlan: @@ -231,3 +246,4 @@ volumes: typesense_data: airflow_logs: unleash_cache: + payload_media: diff --git a/nextjs-app/Dockerfile b/nextjs-app/Dockerfile index 2f4665f..af446c9 100644 --- a/nextjs-app/Dockerfile +++ b/nextjs-app/Dockerfile @@ -53,6 +53,13 @@ COPY --from=builder /app/.next/static ./.next/static # a miss here is a silent 500 on /opengraph-image, not a build failure. COPY --from=builder /app/assets ./assets +# Payload writes uploads here, and the compose file mounts a named volume over +# it. The directory must exist and be owned by the runtime user BEFORE the +# mount: Docker seeds a fresh named volume from the image path, so a missing or +# root-owned directory here makes every upload fail with EACCES at runtime, +# long after the build passed. The chown below covers it. +RUN mkdir -p /app/media + # Set correct permissions RUN chown -R nextjs:nodejs /app