diff --git a/.env.production.example b/.env.production.example index 9f85cb9..6e35307 100644 --- a/.env.production.example +++ b/.env.production.example @@ -2,10 +2,17 @@ # Production environment for the Docker stack. # # cp .env.production.example .env.production -# docker compose run --rm --no-deps app php artisan key:generate --show +# # fill in at least DB_PASSWORD below — docker-compose.yml refuses to +# # interpolate at all without it, even for the next command +# docker compose --env-file .env.production run --rm --no-deps app php artisan key:generate --show # # Everything the containers need is here; docker-compose.yml sets the database, # cache, session and queue drivers itself so they cannot drift. +# +# This file only matters for a manual deploy. A platform that injects its own +# "Environment Variables" (Dokploy, Coolify, ...) exports them as real process +# environment variables instead, which docker-compose.yml's `${VAR}` +# interpolation reads directly — no file needed on the server at all. ############################################################################### APP_NAME="Pireon Admin" diff --git a/README.md b/README.md index 6253ec1..6103cd9 100644 --- a/README.md +++ b/README.md @@ -1499,13 +1499,26 @@ Cuatro contenedores y una base de datos: ```bash cp .env.production.example .env.production -docker compose run --rm --no-deps app php artisan key:generate --show # pégalo en APP_KEY +# Ahora edita .env.production: al menos DB_PASSWORD tiene que tener un valor +# real, porque docker-compose.yml no arranca sin él — ni siquiera para el +# comando siguiente, que solo interpola el fichero entero antes de ejecutar +# nada. APP_KEY puede quedarse vacío por ahora, se rellena con este paso: + +docker compose --env-file .env.production run --rm --no-deps app php artisan key:generate --show # pégalo en APP_KEY docker compose --env-file .env.production up -d --build # el primer administrador, igual que en local docker compose exec app php artisan user:role tu@email.com super_admin ``` +Todas las variables de `docker-compose.yml` llegan por interpolación `${VAR}`, +no por `env_file:` — así que esto funciona igual con `--env-file`, con +variables ya exportadas en el shell, o con el panel de "Environment +Variables" de una plataforma como Dokploy o Coolify, que las inyecta como +variables de entorno reales del proceso que lanza `docker compose`. Ninguna +de las tres formas necesita que exista un fichero `.env.production` en el +servidor. + | Contenedor | Qué hace | |---|---| | `app` | FrankenPHP sirviendo el panel y la API | diff --git a/docker-compose.yml b/docker-compose.yml index be281ee..ba66e4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,31 +6,31 @@ # is one less thing to run, back up and forget to secure. Swapping it in later # is three environment variables. # +# Every application value below comes from `${VAR}` interpolation, not +# `env_file:` — so this works the same way whichever way the variables reach +# `docker compose`: a real shell environment, a platform's own "Environment +# Variables" panel (Dokploy, Coolify, ...), or an explicit `--env-file`: +# # docker compose --env-file .env.production up -d --build # +# `APP_KEY` is deliberately not required here even though the application +# cannot run without it: `docker/entrypoint.sh` checks it instead, with a +# message that says how to generate one. Requiring it at this level would also +# break generating it in the first place — `docker compose run --rm --no-deps +# app php artisan key:generate --show` has to work before `APP_KEY` exists. +# # The web container is the only one that migrates, so a deploy runs them once # rather than once per replica. ############################################################################### +name: landing + x-app: &app build: context: . dockerfile: Dockerfile image: pireon-admin:latest restart: unless-stopped - env_file: .env.production - environment: - DB_CONNECTION: pgsql - DB_HOST: postgres - DB_PORT: 5432 - - # The maintenance flag and the health badge are read on nearly every - # request, so the cache has to be shared by every container — a per-process - # one would have the panel and the API disagreeing about whether the site - # is down. - CACHE_STORE: database - SESSION_DRIVER: database - QUEUE_CONNECTION: database volumes: # Uploaded images live here, and they are the one thing in this stack that # cannot be rebuilt from the repository. @@ -39,17 +39,80 @@ x-app: &app postgres: condition: service_healthy +x-app-env: &app-env + APP_NAME: ${APP_NAME:-Pireon Admin} + APP_ENV: ${APP_ENV:-production} + APP_KEY: ${APP_KEY} + APP_DEBUG: ${APP_DEBUG:-false} + APP_URL: ${APP_URL} + APP_LOCALE: ${APP_LOCALE:-es} + APP_FALLBACK_LOCALE: ${APP_FALLBACK_LOCALE:-es} + + LOG_CHANNEL: ${LOG_CHANNEL:-stack} + LOG_STACK: ${LOG_STACK:-single} + LOG_LEVEL: ${LOG_LEVEL:-warning} + + # Fixed by the service names below, not something a deploy should override — + # otherwise the panel and the API could disagree about the site's state. + DB_CONNECTION: pgsql + DB_HOST: postgres + DB_PORT: 5432 + DB_DATABASE: ${DB_DATABASE:-pireon} + DB_USERNAME: ${DB_USERNAME:-pireon} + DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required. Set it wherever this stack's other variables come from.} + CACHE_STORE: database + SESSION_DRIVER: database + QUEUE_CONNECTION: database + + PIREON_SERVER: ${PIREON_SERVER:-https://oauth.pireon.dev} + PIREON_CLIENT_ID: ${PIREON_CLIENT_ID} + PIREON_CLIENT_SECRET: ${PIREON_CLIENT_SECRET} + PIREON_REDIRECT_URI: ${PIREON_REDIRECT_URI} + PIREON_SCOPES: ${PIREON_SCOPES:-account:read} + PIREON_PASSWORD_GRANT: ${PIREON_PASSWORD_GRANT:-true} + PIREON_INTROSPECTION_TTL: ${PIREON_INTROSPECTION_TTL:-60} + PIREON_DEFAULT_ROLE: ${PIREON_DEFAULT_ROLE:-viewer} + PIREON_REQUIRE_ACTIVE: ${PIREON_REQUIRE_ACTIVE:-true} + PIREON_REQUIRE_APPROVED: ${PIREON_REQUIRE_APPROVED:-true} + + SITE_URL: ${SITE_URL} + SITE_PREFIX_DEFAULT_LOCALE: ${SITE_PREFIX_DEFAULT_LOCALE:-false} + SITE_CACHE_SECONDS: ${SITE_CACHE_SECONDS:-60} + SITE_PREVIEW_TOKEN: ${SITE_PREVIEW_TOKEN} + SITE_RATE_LIMIT: ${SITE_RATE_LIMIT:-600} + SITE_TRUSTED_IPS: ${SITE_TRUSTED_IPS:-} + SITE_INGEST_TOKEN: ${SITE_INGEST_TOKEN} + SITE_TELEMETRY_RATE_LIMIT: ${SITE_TELEMETRY_RATE_LIMIT:-60} + SITE_TELEMETRY_MAX_BATCH: ${SITE_TELEMETRY_MAX_BATCH:-50} + SITE_ACCEPT_SUBMISSIONS: ${SITE_ACCEPT_SUBMISSIONS:-true} + SITE_HONEYPOT_FIELD: ${SITE_HONEYPOT_FIELD:-website} + SITE_SUBMISSION_RATE_LIMIT: ${SITE_SUBMISSION_RATE_LIMIT:-3} + SITE_SUBMISSION_HOURLY_LIMIT: ${SITE_SUBMISSION_HOURLY_LIMIT:-60} + SITE_SUBMISSION_DEDUPE_MINUTES: ${SITE_SUBMISSION_DEDUPE_MINUTES:-1440} + SITE_SUBMISSION_MAIL: ${SITE_SUBMISSION_MAIL:-false} + + MEDIA_DISK: ${MEDIA_DISK:-public} + MEDIA_MAX_PIXELS: ${MEDIA_MAX_PIXELS:-50000000} + + GIT_PROVIDER: ${GIT_PROVIDER:-gitea} + GITEA_URL: ${GITEA_URL:-} + GITEA_TOKEN: ${GITEA_TOKEN:-} + GITEA_ORG: ${GITEA_ORG:-} + GIT_SYNC_CRON: ${GIT_SYNC_CRON:-hourly} + + MAIL_MAILER: ${MAIL_MAILER:-log} + MAIL_HOST: ${MAIL_HOST:-} + MAIL_PORT: ${MAIL_PORT:-587} + MAIL_USERNAME: ${MAIL_USERNAME:-} + MAIL_PASSWORD: ${MAIL_PASSWORD:-} + MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-admin@example.org} + MAIL_FROM_NAME: ${MAIL_FROM_NAME:-Pireon Admin} + services: app: <<: *app environment: - DB_CONNECTION: pgsql - DB_HOST: postgres - DB_PORT: 5432 - CACHE_STORE: database - SESSION_DRIVER: database - QUEUE_CONNECTION: database - + <<: *app-env # Only here: migrations run once per deploy, not once per container. RUN_MIGRATIONS: "true" ports: @@ -67,6 +130,7 @@ services: worker: <<: *app command: ["php", "artisan", "queue:work", "--tries=3", "--max-time=3600", "--sleep=3"] + environment: *app-env # Waits for the web container, not just the database: it is the one that # migrates, and a worker that starts against an empty schema spends its @@ -89,6 +153,7 @@ services: scheduler: <<: *app command: ["php", "artisan", "schedule:work"] + environment: *app-env # The FrankenPHP base image ships a healthcheck that probes Caddy's admin # API, and this container does not serve HTTP — inherited, it could never @@ -119,7 +184,7 @@ services: environment: POSTGRES_DB: ${DB_DATABASE:-pireon} POSTGRES_USER: ${DB_USERNAME:-pireon} - POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env.production} + POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required. Set it wherever this stack's other variables come from.} # Content is written by a handful of people and read constantly; the # default collation rules are what the JSON slug lookups sort under.