Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 2s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 31s
CI / integration (push) Successful in 3m44s
docker-compose.yml pinned :dev on all five app services — web, worker, scheduler, maintenance-long, ml-worker. The README documents `docker compose -f docker-compose.yml up -d` as the production path, and -f means "use only this file", skipping the override and its build: directives. So Compose pulled image:, and image: was the rolling development channel. The documented way to install this product shipped development builds. It went unnoticed for a structural reason rather than a careless one: nobody who works on the project takes that path. The operator deploys from a swarm stack file; contributors get docker-compose.override.yml, which sets build: for all five services, and build: wins over image:. The broken path is reachable only by a stranger following the README — which is exactly the audience that did not exist until now. :latest, per rule 147: main IS production. It is also what the agent stack (agent/docker-compose.yml) already pinned, so this makes the two stacks agree rather than introducing a new convention. Both paths verified with `docker compose config`, which merges and prints without starting anything: dev path — build: present on all five, image: not pulled -f production — 0 build: directives, five :latest images resolved Also checked the base file for anything a stranger could not satisfy: no host-absolute volume paths, no operator-specific port bindings, no device mappings. The tag was the only defect in the consumer path. The comment on web.image is deliberately long (rule 32). A line reading :latest inside a file a developer is debugging with is exactly the line someone flips back to :dev to test something and then commits, and the consequence — strangers silently installing bleeding edge — is invisible to everyone who works here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QHszn9H8VBvx5Ke8x1hvw
138 lines
7.1 KiB
Markdown
138 lines
7.1 KiB
Markdown
# FabledCurator
|
|
|
|
Self-hosted media curation — gallery, ML tagging, and subscription-driven downloading in one app. Part of the FabledSword family.
|
|
|
|
Combines what was [ImageRepo](https://git.fabledsword.com/bvandeusen/ImageRepo) (gallery, ML, importer) and [GallerySubscriber](https://git.fabledsword.com/bvandeusen/GallerySubscriber) (gallery-dl wrapper, subscriptions, credential capture) into a single product.
|
|
|
|
## Status
|
|
|
|
In production. `main` is continuously deployed — every merge to `main` builds
|
|
and publishes `:latest` images, so whatever is on `main` is what is running.
|
|
Day-to-day work happens on `dev`, which publishes `:dev` images.
|
|
|
|
## Versions and tags
|
|
|
|
Three image tags exist, and no others:
|
|
|
|
| Tag | Branch | Meaning |
|
|
| --- | --- | --- |
|
|
| `:latest` | `main` | Production. Moves on every merge. |
|
|
| `:c-<sha>` | `main` | Immutable — the rollback unit, all three images together. |
|
|
| `:dev` | `dev` | The rolling test channel. Moves on every push. |
|
|
|
|
There are deliberately **no version tags**. Nothing pins one, and a per-build
|
|
name nobody reads is upkeep for a model FC does not run (family rule 145; the
|
|
reasoning is note #3127 §5). Rolling back is `docker pull …:c-<sha>`.
|
|
|
|
Each artifact still has a version, derived rather than chosen: the commit time
|
|
of the newest change to that artifact's *own* shipped files, as
|
|
`YYYY.MM.DD.HHMM` UTC (rule 148). Four artifacts, four independent versions —
|
|
a push touching only `agent/` re-versions the agent and leaves web and ml
|
|
alone, and CI skips the builds whose content did not move.
|
|
|
|
Because no registry name carries it, the running instance's own report is the
|
|
only answer to "which build is this?". The foot of Settings shows
|
|
`FabledCurator 2026.08.29.0201 · dev`, and `/api/health` returns the same two
|
|
fields.
|
|
|
|
Release tags are optional bookmarks — FC went twelve weeks without one and
|
|
nothing was wrong. Pushing `v<version>` publishes a Forgejo release listing the
|
|
commits since the previous tag; it builds no image.
|
|
|
|
## What's in here
|
|
|
|
Five deployable pieces, built by `.forgejo/workflows/build.yml`:
|
|
|
|
| Piece | Built from | Image | Role |
|
|
| --- | --- | --- | --- |
|
|
| **Web / workers** | `Dockerfile` | `fabledcurator` | Quart API + the built Vue SPA in one image. `entrypoint.sh` picks the role: `web`, `worker`, `scheduler`. The `maintenance-long` service is a second `worker` pinned to the long-running maintenance queue. |
|
|
| **ML worker** | `Dockerfile.ml` | `fabledcurator-ml` | Same app, plus `requirements-ml.txt` — tagging and embedding models that run in-container. |
|
|
| **GPU agent** | `agent/Dockerfile` | `fabledcurator-agent` | Optional desktop-GPU worker (`agent/`). Leases jobs over **HTTP only** — never touches the database or Redis. Run it for a burst, stop it to reclaim the card. See `agent/README.md`. |
|
|
| **Firefox extension** | `extension/` | signed XPI | MV3 extension: pushes platform session cookies into FC and adds a creator as a Source in one click. AMO-signed on both `dev` and `main` (one signature per extension change, shared by the two channels), bundled into that channel's web image and served from Settings → Maintenance. See `extension/README.md`. |
|
|
| **Data** | — | `pgvector/pgvector:pg16`, `redis:7-alpine` | Postgres with pgvector for embeddings; Redis as the Celery broker. |
|
|
|
|
## Quick start
|
|
|
|
For local development and testing, just:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
# UI: http://localhost:8080
|
|
```
|
|
|
|
That uses sane dev defaults baked into `docker-compose.yml` and the dev
|
|
override (`docker-compose.override.yml`, auto-merged) — local builds, DEBUG
|
|
logging, exposed Postgres + Redis ports on the host. No `.env` required.
|
|
|
|
For a production-like deployment, override the dev defaults via shell env
|
|
or a `.env` file (see `.env.example` for the variable names) and use:
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml up -d
|
|
# (skips the dev override, so containers pull published :latest images)
|
|
```
|
|
|
|
`-f` is doing real work there: it tells Compose to use *only* that file, which
|
|
skips `docker-compose.override.yml` and its local builds. What you get is the
|
|
`:latest` images — the stable channel, built from `main`. This is the install
|
|
path, and it is the one to use if you are running FabledCurator rather than
|
|
working on it.
|
|
|
|
`:dev` is the other channel: rebuilt from the `dev` branch several times a day,
|
|
bleeding edge, no stability promise. Nothing in this repo points an installer at
|
|
it, and nothing should.
|
|
|
|
The GPU agent is deployed separately, on the machine with the card —
|
|
`agent/docker-compose.yml`, not this stack.
|
|
|
|
## Deployment posture
|
|
|
|
FabledCurator is designed to run inside a self-hosted homelab environment over plain HTTP. If you want TLS, terminate it at your reverse proxy. The app does not generate certificates, redirect to HTTPS, or set HSTS.
|
|
|
|
## CI / Forgejo setup
|
|
|
|
Four workflows: `ci.yml` (lint, extension-version check, backend unit tests,
|
|
frontend build, integration), `extension.yml` (extension lint, vitest, XPI
|
|
content verification), `build.yml` (sign + publish), and `release.yml`, which
|
|
runs only on a `v*` tag and publishes a changelog without building anything.
|
|
|
|
**The toolchain each job runs in is its `container.image`, not its `runs-on`
|
|
label.** `runs-on: python-ci` only schedules the job onto a runner; every job
|
|
then names the image it actually wants. `ci-requirements.md` is the current,
|
|
authoritative list of images and per-job installs — read that rather than a
|
|
copy here, so the two can't drift.
|
|
|
|
The repo expects one secret:
|
|
|
|
- **`RELEASE_TOKEN`** — a Forgejo PAT with:
|
|
- `write:package` + `read:package` — for `docker push` to `git.fabledsword.com`
|
|
- `write:release` — for the `ext-<version>` releases that cache the signed XPI
|
|
- `write:issue` — for issue-management automation
|
|
|
|
Generate at https://git.fabledsword.com/user/settings/applications. The injected `GITHUB_TOKEN` cannot be used because it lacks `write:package`.
|
|
|
|
AMO signing additionally needs `MOZILLA_AMO_JWT_KEY` / `MOZILLA_AMO_JWT_SECRET`.
|
|
It runs on **both** channels and is cached per version: because the version is
|
|
derived from commit time, `dev` and `main` derive the same number for the same
|
|
source, so `main` finds `dev`'s signature already cached and makes no second AMO
|
|
call. That cache is why signing must be one-shot — AMO rejects a re-signed
|
|
version.
|
|
|
|
## License
|
|
|
|
**GNU Affero General Public License v3.0** — see [LICENSE](LICENSE).
|
|
|
|
You may run, study, modify and redistribute this software. The condition is
|
|
reciprocity: if you distribute a modified version, or **run one as a network
|
|
service that other people use**, you must offer those users the corresponding
|
|
source under the same licence. That second clause (AGPL §13) is the reason this
|
|
licence rather than the GPL — for a self-hosted web application, "distribution"
|
|
otherwise never happens, and the obligation would never bite.
|
|
|
|
Running an unmodified copy for yourself, your household or your organisation
|
|
carries no obligation at all. Neither does modifying it privately. The licence
|
|
asks something of you only when you hand your modified version to others.
|
|
|
|
Contributions ship under the same licence — see [CONTRIBUTING](CONTRIBUTING.md).
|
|
Security reports: [SECURITY.md](SECURITY.md).
|