From be853e3017a968f432a0c4297b3fb7fefa2da336 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 23 Apr 2026 16:30:38 +0200 Subject: [PATCH] update remote build --- .env.dist | 5 ++- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++---- compose.hub.yaml | 40 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/.env.dist b/.env.dist index aad4d46..de71349 100644 --- a/.env.dist +++ b/.env.dist @@ -39,7 +39,10 @@ MYSQL_ROOT_PASSWORD=root_password # PREWARM_ON_START=0 # Hub deploy: optional full image ref (default silberengel/unfold:latest in compose.hub.yaml). # UNFOLD_DOCKER_IMAGE=silberengel/unfold:1.0.0 -# Optional extra CLI args for the docker `cron` service (full `app:prewarm` every 10 min). Example: --metadata-limit=100 --no-magazine +# Optional extra CLI args for the docker `cron` service (dev) and `prewarm` service (compose.hub.yaml): +# full `app:prewarm` every 10 min. Example: --metadata-limit=100 --no-magazine +# After changing, recreate: `docker compose up -d --force-recreate cron` (dev) or +# `docker compose -f compose.hub.yaml up -d --force-recreate prewarm` (hub). # PREWARM_FLAGS= # compose.hub.yaml: default host port is 9080. Use 80 only if nothing else binds it. Loopback-only example: # HTTP_PUBLISH=127.0.0.1:9080 diff --git a/README.md b/README.md index 1859179..462f672 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ make prewarm | 3 | `articles:get -- '-2 month' 'now'` — sync long-form into MySQL for that window | | 4 | `app:prewarm` — magazine **30040**, **kind-0** profiles, **comment** cache (default **`--comments-max=10`**, newest by `createdAt`) | -`make prewarm` brings the stack (including `cron`) up so scheduled prewarm is active. **Optional** extra arguments for the **cron**-scheduled `app:prewarm` go in **`.env`** as **`PREWARM_FLAGS`** (same as you might pass to `php bin/console app:prewarm …`); Compose passes them into the `cron` container. Example: `PREWARM_FLAGS="--metadata-limit=50 --no-magazine"`. **Restart** the `cron` service after changing `PREWARM_FLAGS` so the container reloads the env. Hub / `compose.hub.yaml` has no `cron` service; use a host timer or `exec` if you need the same there. +`make prewarm` brings the stack (including `cron`) up so scheduled prewarm is active. **Optional** extra arguments for the **cron**-scheduled `app:prewarm` go in **`.env`** as **`PREWARM_FLAGS`** (same as you might pass to `php bin/console app:prewarm …`); Compose passes them into the `cron` container. Example: `PREWARM_FLAGS="--metadata-limit=50 --no-magazine"`. **Restart** the `cron` service after changing `PREWARM_FLAGS` so the container reloads the env. On the **hub** stack, the `prewarm` service reads the same `PREWARM_FLAGS`; use `docker compose -f compose.hub.yaml up -d --force-recreate prewarm` after changing it. --- @@ -107,15 +107,84 @@ For a full **Nostr backfill** + one-shot prewarm, use **`make prewarm`** (or a h --- -## Production / Hub image +## Production / Hub (remote server) + +The app runs as a **pre-built** image (no app source on the server). The server only needs `compose.hub.yaml`, a `.env`, and Docker. Default image: `silberengel/unfold:latest`; override with **`UNFOLD_DOCKER_IMAGE`**. | Topic | Notes | |-------|--------| -| `compose.hub.yaml` | Runs a **pulled** image (default `silberengel/unfold:latest`), no local PHP app build. Override with `UNFOLD_DOCKER_IMAGE`. | -| HTTP publish | `HTTP_PUBLISH` in `.env` (default **9080** → container **80**). Set `TRUSTED_PROXIES` behind a reverse proxy. | -| Secrets | Set `APP_SECRET` and DB credentials in **real** env; do not commit production secrets. | +| `compose.hub.yaml` | Defines **`php`** (FrankenPHP) + **`database`** (MySQL) + **`prewarm`** (same app image: **`app:prewarm` every 10 minutes**, like dev’s `docker/cron`). Optional: disable `prewarm` in Compose if you prefer a host `cron` only. | +| HTTP | **`HTTP_PUBLISH`** in `.env` maps **host** port → container **80** (default **9080**). Put a reverse proxy (e.g. Apache) in front; set **`TRUSTED_PROXIES`** to match your proxy (often include `127.0.0.0/8` and the Docker bridge CIDR, e.g. `172.16.0.0/12`). | +| Secrets | Real **`APP_SECRET`** and **`MYSQL_*`** (or external DB via `DATABASE_URL` if you change the file). Do not commit production `.env`. | +| `PREWARM_FLAGS` | Optional extra CLI args for the hub **`prewarm`** service (and dev **`cron`**). After editing `.env`, run `docker compose -f compose.hub.yaml up -d --force-recreate prewarm`. | + +### Build, tag, and push (on your machine or CI) + +From the **repository root** (same `Dockerfile` as local prod): + +```bash +# Production image +docker build --platform linux/amd64 --target frankenphp_prod -t YOUR_REGISTRY/unfold:latest . + +# Optional: immutable tag for rollbacks +docker build --platform linux/amd64 --target frankenphp_prod -t YOUR_REGISTRY/unfold:1.0.0 . + +# Push what you use on the server +docker push YOUR_REGISTRY/unfold:latest +docker push YOUR_REGISTRY/unfold:1.0.0 +``` + +- Use **`linux/amd64`** if the server is amd64; use **`arm64`** (or a matching `--platform`) for arm servers. +- The image name must match what the server will pull: either keep **`UNFOLD_DOCKER_IMAGE=YOUR_REGISTRY/unfold:TAG`** in server `.env`, or push to the default name **`silberengel/unfold:latest`**. + +### Deploy on the server (pull, up, migrate) + +In a directory that contains **only** `compose.hub.yaml` and your **`.env`** (e.g. `~/tmp/unfold`): + +```bash +cd /path/to/deploy +docker compose -f compose.hub.yaml pull +docker compose -f compose.hub.yaml up -d +docker compose -f compose.hub.yaml exec php php bin/console doctrine:migrations:migrate --no-interaction +``` + +After code changes: **`pull` → `up -d`**; run **migrations** when the repo added new migration files. + +**Optional image / tag** (in `.env` or one-shot): + +```bash +export UNFOLD_DOCKER_IMAGE=YOUR_REGISTRY/unfold:1.0.0 +docker compose -f compose.hub.yaml up -d +``` + +### One-time Nostr backfill (equivalent to `make prewarm` on dev) + +`compose.hub` has no bind-mounted repo, so run the same commands **inside the `php` container** (after the stack is up and migrations have run): + +```bash +docker compose -f compose.hub.yaml exec -T php php bin/console articles:get -- '-2 month' 'now' +docker compose -f compose.hub.yaml exec -T php php bin/console app:prewarm +``` + +Adjust the **articles:get** window as needed. + +### Scheduled `app:prewarm` on hub + +The **`prewarm`** service in `compose.hub.yaml` uses the **same** image as `php` and runs **`app:prewarm` every 10 minutes** (same cadence as dev’s `docker/cron`). It starts only after the **database** is healthy and the **`php`** service passes its healthcheck (so migrations from the `php` entrypoint have typically completed). **Optional** `PREWARM_FLAGS` in `.env` is passed into that container; after changing it, run: + +```bash +docker compose -f compose.hub.yaml up -d --force-recreate prewarm +``` + +**If you do not** want a Compose sidecar (e.g. to save RAM), stop and disable the `prewarm` service and use **host** `cron` or **systemd** instead: + +```text +*/10 * * * * cd /path/to/deploy && docker compose -f compose.hub.yaml exec -T php php bin/console app:prewarm +``` + +**`PREWARM_ON_START=1`** on the `php` service only warms **once** at container start, not on a schedule. -File header in `compose.hub.yaml` lists pull, migrate, and optional build/push one-liners. +The file `compose.hub.yaml` in the repo repeats minimal pull/migrate/build one-liners in its header for quick copy-paste. --- diff --git a/compose.hub.yaml b/compose.hub.yaml index 11a25ad..b728116 100644 --- a/compose.hub.yaml +++ b/compose.hub.yaml @@ -5,6 +5,9 @@ # docker compose -f compose.hub.yaml up -d # docker compose -f compose.hub.yaml exec php php bin/console doctrine:migrations:migrate --no-interaction # +# Services: `php` (web), `database` (MySQL), `prewarm` (same image; `app:prewarm` every 10 min — see README). +# Optional: PREWARM_FLAGS in .env (same as dev `cron` service), then `docker compose up -d --force-recreate prewarm`. +# # Required in .env: APP_SECRET. Set MYSQL_* (or replace DATABASE_URL after editing this file) if you # use the bundled database. For TLS in front, set TRUSTED_PROXIES to include your reverse proxy CIDR. # @@ -35,10 +38,47 @@ services: - caddy_config:/config ports: - "${HTTP_PUBLISH:-9080}:80/tcp" + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1/", "-o", "/dev/null"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 120s depends_on: database: condition: service_healthy + prewarm: + image: ${UNFOLD_DOCKER_IMAGE:-silberengel/unfold:latest} + pull_policy: always + restart: unless-stopped + working_dir: /app + # Same app image as `php`, but not FrankenPHP: wait for DB + `php` healthy (migrations done), then + # run `app:prewarm` every 10 minutes (dev `docker/cron` uses the same interval). + entrypoint: ["/bin/sh", "-c"] + command: + - | + until php bin/console dbal:run-sql -q "SELECT 1" 2>/dev/null; do + echo "prewarm: waiting for database..." + sleep 2 + done + while true; do + sleep 600 + php bin/console app:prewarm $${PREWARM_FLAGS-} || true + done + environment: + APP_ENV: ${APP_ENV:-prod} + APP_SECRET: ${APP_SECRET} + TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.0/8,10.0.0.0/8} + SERVER_NAME: ${SERVER_NAME:-:80} + DATABASE_URL: mysql://${MYSQL_USER:-unfold_user}:${MYSQL_PASSWORD:-password}@database:3306/${MYSQL_DATABASE:-unfold_db}?serverVersion=${MYSQL_VERSION:-8.0}&charset=${MYSQL_CHARSET:-utf8mb4} + PREWARM_FLAGS: ${PREWARM_FLAGS:-} + depends_on: + database: + condition: service_healthy + php: + condition: service_healthy + database: image: mysql:${MYSQL_VERSION:-8.0} restart: unless-stopped