|
|
4 days ago | |
|---|---|---|
| assets | 4 days ago | |
| bin | 5 days ago | |
| config | 4 days ago | |
| deploy | 5 days ago | |
| docker/cron | 5 days ago | |
| docs | 9 months ago | |
| frankenphp | 4 days ago | |
| migrations | 5 days ago | |
| public | 5 days ago | |
| publication/Newsroom | 9 months ago | |
| scripts | 5 days ago | |
| src | 4 days ago | |
| templates | 4 days ago | |
| tests | 11 months ago | |
| translations | 9 months ago | |
| .dockerignore | 6 days ago | |
| .editorconfig | 1 year ago | |
| .env.dist | 5 days ago | |
| .env.test | 1 year ago | |
| .gitattributes | 1 year ago | |
| .gitignore | 12 months ago | |
| Dockerfile | 4 days ago | |
| LICENSE | 1 year ago | |
| Makefile | 5 days ago | |
| Makefile.hub | 5 days ago | |
| README.md | 5 days ago | |
| compose.hub.yaml | 4 days ago | |
| compose.override.yaml | 5 days ago | |
| compose.prod.yaml | 9 months ago | |
| compose.yaml | 4 days ago | |
| composer.json | 4 days ago | |
| composer.lock | 4 days ago | |
| importmap.php | 5 days ago | |
| package.json | 1 year ago | |
| phpunit.xml.dist | 1 year ago | |
| symfony.lock | 4 days ago | |
README.md
Unfold: Imwald
A Symfony + FrankenPHP site that reads Nostr long-form articles (kind 30023) and related data from relays, stores articles in MySQL, and serves pages with Twig. Comments and profile metadata are cache-backed (not the full source of truth in the DB).
Requirements
| Requirement | Version / notes |
|---|---|
| PHP | ≥ 8.3.13 (see composer.json) |
| Docker | Optional; recommended for local dev and production images |
| Database | MySQL 8.0 (configurable) |
Local development (Docker)
-
Env: copy
.env.distto.envand adjust if needed (especiallyAPP_SECREToutside dev). -
Start stack
docker compose up -d -
App URL (default): http://127.0.0.1:9080
Port comes fromHTTP_PORTin.envandcompose.override.yaml(loopback only). -
First-time DB: migrations run on php container start when
migrations/contains PHP files (seefrankenphp/docker-entrypoint.sh).
| Service | Role |
|---|---|
php |
FrankenPHP + Caddy, Symfony app, console |
database |
MySQL; dev exposes 127.0.0.1:3307 → 3306 for local clients |
cron |
Runs full app:prewarm every 10 minutes; repo bind-mounted at /var/www/html (see docker/cron/) |
Backfill articles + warm caches (recommended)
To migrate, import articles from Nostr for a time window, then prewarm magazine indices, author metadata, and comment caches:
make prewarm
| Step (script order) | Command / effect |
|---|---|
| 1 | docker compose up -d --wait — starts php, database, and cron (the cron image runs a full app:prewarm on a 10 min schedule) |
| 2 | doctrine:migrations:migrate |
| 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. 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.
Console commands (overview)
| Command | Purpose |
|---|---|
articles:get <from> <to> |
Pull long-form articles from Nostr for the time range, persist to DB |
app:prewarm |
Magazine relay refresh + metadata cache + comment cache warm |
doctrine:migrations:migrate |
Apply SQL migrations |
user:elevate |
(If used) user elevation helper |
php bin/console list and … -h for full options.
app:prewarm (notable options)
| Option | Default | Meaning |
|---|---|---|
--no-magazine |
off | Skip magazine 30040 index |
--no-metadata |
off | Skip Nostr kind-0 / profile cache |
--no-comments |
off | Skip comment thread cache |
--metadata-limit |
0 (all authors) |
Cap distinct author pubkeys |
--metadata-batch |
50 |
Pubkeys per batched Nostr REQ |
--comments-max |
10 |
Newest N articles (by createdAt DESC); 0 = all (still bounded by budget) |
--comments-budget |
600 |
Max wall seconds for the whole comments phase (Nostr is slow; raise e.g. 1200 if you need more articles in one run) |
--magazine-budget |
90 |
Max wall seconds for magazine root + per-category 30040 fetches (hard-capped at 600s in code). If you have many categories, a low budget can stop before the last slug is refreshed—stale home/category pages until the next run. Set MAGAZINE_PREWARM_PREFER_SLUGS (comma-separated category #d slugs) to fetch those first after the root. |
Prewarm clears the PHP CLI execution time limit for that run; relay work can be slow.
PREWARM_ON_START (optional)
| Variable | Set where | Effect |
|---|---|---|
PREWARM_ON_START=1 |
Compose environment on the php service (not only Symfony .env inside the container) |
After DB is up and migrations run, executes app:prewarm once on start. Does not run articles:get. |
For a full Nostr backfill + one-shot prewarm, use make prewarm (or a host cron / systemd timer) instead of relying on PREWARM_ON_START alone.
Configuration
| What | File |
|---|---|
Site title, npub, d_tag, relays (default_relay, article_relays, profile_relays), theme |
config/unfold.yaml (imported as Symfony parameters) |
MAGAZINE_PREWARM_PREFER_SLUGS |
.env / .env.local — optional comma-separated category slugs to prioritize in app:prewarm magazine phase (after the root). Use when the relay time budget would otherwise skip your updated category. |
DATABASE_URL, APP_SECRET, HTTP_PORT, MYSQL_*, optional PREWARM_FLAGS (for the Docker cron service) |
.env / .env.local (see .env.dist) |
Service wiring (e.g. cache, NostrClient args) |
config/services.yaml |
Relays (short): default_relay and article_relays drive article sync and many queries; profile_relays are used first for kind-0 / profile fetches, then the merged default + article set (see NostrClient).
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 |
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):
# 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/amd64if the server is amd64; usearm64(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:TAGin server.env, or push to the default namesilberengel/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):
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.
Makefile.hub (on the server)
Copy Makefile.hub into the same directory as compose.hub.yaml and .env (no full clone required). You get short commands like the dev Makefile, all using docker compose -f compose.hub.yaml under the hood:
make -f Makefile.hub help # list targets
make -f Makefile.hub pull
make -f Makefile.hub up
make -f Makefile.hub migrate
make -f Makefile.hub prewarm-once
make -f Makefile.hub articles-get # optional: ARTICLES_FROM='-1 year' ARTICLES_TO=now
make -f Makefile.hub backfill # up + migrate + articles-get + prewarm-once (closest to dev `make prewarm`)
Optional image / tag (in .env or one-shot):
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)
Use make -f Makefile.hub backfill, or run the same inside the php container:
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 (see Makefile.hub / ARTICLES_FROM / ARTICLES_TO).
Scheduled app:prewarm on hub
The prewarm service uses the same image as php and runs app:prewarm every 10 minutes (same cadence as dev’s docker/cron). It waits for MySQL and the doctrine_migration_versions table (so the php entrypoint has run migrations) — it does not use curl to the php service, which can fail if HTTP is only bound for loopback inside that container. Optional PREWARM_FLAGS in .env is passed into that container; after changing it, run:
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:
*/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.
The file compose.hub.yaml in the repo repeats minimal pull/migrate/build one-liners in its header for quick copy-paste.
License
MIT — see LICENSE.
Project links (example)
Configurable under parameters.external_links in config/unfold.yaml (e.g. Unfold on GitHub, Decent Newsroom). Adjust for your deployment.