build(cms): wire Payload into the Docker image and both stacks
Uploads go to a named volume at /app/media. The directory is created in the image before the mount and covered by the existing chown, because Docker seeds a fresh named volume from the image path — a missing or root-owned directory there fails every upload with EACCES at runtime, long after the build passed. PAYLOAD_SECRET uses the same :? form as AIRFLOW_ADMIN_PASSWORD: refuse to start rather than boot with an empty secret and accept forged sessions. Staging's must differ from production's, which the header comment now says explicitly. Portainer prefixes volume names per stack, so payload_media isolates itself. prodMigrations is not wired yet — generating the initial migration needs a reachable Postgres. Follows in its own commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017YmbBhr8s7GusjDE12hrZM
This commit is contained in:
1 parent
c2c76c5817
commit
310b63b0cb
3 files changed
+39
No files matched your search
@@ -18,6 +18,10 @@
|
|||||||
# TYPESENSE_SEARCH_KEY — Typesense search-only key (exposed to frontend)
|
# TYPESENSE_SEARCH_KEY — Typesense search-only key (exposed to frontend)
|
||||||
# UNLEASH_URL — http://<unleash-ip>:4242/api (empty = all flags off)
|
# UNLEASH_URL — http://<unleash-ip>:4242/api (empty = all flags off)
|
||||||
# UNLEASH_API_TOKEN — Unleash *client* token, environment: development
|
# 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_USER — Airflow admin username (default: admin)
|
||||||
# AIRFLOW_ADMIN_PASSWORD — Airflow admin password. REQUIRED: the api-server
|
# AIRFLOW_ADMIN_PASSWORD — Airflow admin password. REQUIRED: the api-server
|
||||||
# refuses to start without it, rather than falling
|
# refuses to start without it, rather than falling
|
||||||
@@ -89,9 +93,20 @@ services:
|
|||||||
- FASTAPI_URL=http://backend:80/api
|
- FASTAPI_URL=http://backend:80/api
|
||||||
- TYPESENSE_URL=http://typesense:8108
|
- TYPESENSE_URL=http://typesense:8108
|
||||||
- TYPESENSE_API_KEY=${TYPESENSE_SEARCH_KEY:-changeme}
|
- 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:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
sc_database:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
backend: {}
|
backend: {}
|
||||||
macvlan:
|
macvlan:
|
||||||
@@ -242,3 +257,4 @@ volumes:
|
|||||||
typesense_data:
|
typesense_data:
|
||||||
airflow_logs:
|
airflow_logs:
|
||||||
unleash_cache:
|
unleash_cache:
|
||||||
|
payload_media:
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
# TYPESENSE_SEARCH_KEY — Typesense search-only key (exposed to frontend)
|
# TYPESENSE_SEARCH_KEY — Typesense search-only key (exposed to frontend)
|
||||||
# UNLEASH_URL — http://<unleash-ip>:4242/api (empty = all flags off)
|
# UNLEASH_URL — http://<unleash-ip>:4242/api (empty = all flags off)
|
||||||
# UNLEASH_API_TOKEN — Unleash *client* token, environment: production
|
# 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_USER — Airflow admin username (default: admin)
|
||||||
# AIRFLOW_ADMIN_PASSWORD — Airflow admin password. REQUIRED: the api-server
|
# AIRFLOW_ADMIN_PASSWORD — Airflow admin password. REQUIRED: the api-server
|
||||||
# refuses to start without it, rather than falling
|
# refuses to start without it, rather than falling
|
||||||
@@ -78,9 +81,21 @@ services:
|
|||||||
- FASTAPI_URL=http://backend:80/api
|
- FASTAPI_URL=http://backend:80/api
|
||||||
- TYPESENSE_URL=http://typesense:8108
|
- TYPESENSE_URL=http://typesense:8108
|
||||||
- TYPESENSE_API_KEY=${TYPESENSE_SEARCH_KEY:-changeme}
|
- 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:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
sc_database:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
backend: {}
|
backend: {}
|
||||||
macvlan:
|
macvlan:
|
||||||
@@ -231,3 +246,4 @@ volumes:
|
|||||||
typesense_data:
|
typesense_data:
|
||||||
airflow_logs:
|
airflow_logs:
|
||||||
unleash_cache:
|
unleash_cache:
|
||||||
|
payload_media:
|
||||||
@@ -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.
|
# a miss here is a silent 500 on /opengraph-image, not a build failure.
|
||||||
COPY --from=builder /app/assets ./assets
|
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
|
# Set correct permissions
|
||||||
RUN chown -R nextjs:nodejs /app
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user