Docker¶
Paperful is a local workbench. The operator install is git clone and
docker compose build, then docker compose up at
http://127.0.0.1:8765. Build the image on your computer (paperful:local)
with docker compose build. Zotero and headed browser login stay on the
host; the container talks to them over the local API.
docker compose up keeps the GUI running. CLI one-shots are
docker compose run --rm paperful … (the image is also a pack for unattended
run, lint, ocr, report, attach once a write key exists). Python 3.12,
Poppler (pdftotext), OCRmyPDF and Tesseract (eng), Playwright Chromium.
Build mode is selected with PAPERFUL_IMAGE_MODE (see below). Contributors can
still use uv on the host (see the
README Develop section).
Jobs in the GUI: Walkthroughs.
Durable data — config, custom playbook packs, out/, and state/ — still
lives outside the container (and, by default, outside the git root).
Mounts (stranger path): host PAPERFUL_DATA → container /data
(config.toml, state/, packs). Host PAPERFUL_OUT → /data/out (PDF
mirror). Separate mounts by default. Compose does not mount Zotero Storage
writable. See Terms.
What CI proves: the GitHub gate is pytest -m smoke on Ubuntu. It does
not build the image. Build it yourself (docker compose build) and run
doctor --no-guide with no live Zotero. Exit 2 means the Zotero row
is red, which is a first-run stranger machine, not a failed Compose install.
Putting that check back on CI is step 5 of the
CI rebuild.
What still runs on the host¶
Zotero (GUI, local API, “Always allow”)
paperful session login scholar|ezproxy(headed Chrome/Edge on the host: campus SSO, Scholar CAPTCHA). An everyday-browser login does not transfer — see Sessionspaperful session login mendeley(Elsevier OAuth; the localhost redirect will not reach a container — see Mendeley)EndNote
.enl/.Data(desktop library on the host; Paperful never writes SQLite — see EndNote)Any scripts that read
out//state/as filesOptional
valeforcritique --vale(not in the image;doctorambers if missing)
The container talks to host Zotero over the local API (:23119). Session
vaults and the write key must be the same state/ tree the container
mounts (PAPERFUL_DATA).
Prerequisites¶
Docker with Compose v2
Zotero running on the host, local API enabled (Settings → Advanced → Allow other applications on this computer to communicate with Zotero)
For attach /
fix-metadata --apply: complete the Zotero “Always allow” dialog once on the host (key is stored understate/)For Scholar / EZProxy sessions: a host
uvinstall so you can runpaperful session login …, then reuse the mountedstate/sessions/from the container.doctorprints fix steps; usedoctor --guideviadocker compose run --rm paperful doctor --guidefor step-by-step re-check.
Quick start¶
git clone --branch v1.9.0 https://github.com/glen-w/Paperful.git
cd Paperful
cp .env.example .env
Keep your current repo-local setup (no data move):
cp .env.example .env
# edit .env: PAPERFUL_DATA=. and PAPERFUL_OUT=./out
docker compose build
docker compose run --rm paperful doctor
# Without host Zotero this exits 2 (red Zotero row). That is expected; CI job docker
# asserts the same. Start Zotero, then re-run doctor.
docker compose up
# → http://127.0.0.1:8765 (Wanted). CLI one-shots stay compose run.
docker compose run --rm paperful run --collection interesting --dry-run
Or migrate into sibling mounts (recommended if you use Compose long-term; same layout as paperful.app):
cp .env.example .env # PAPERFUL_DATA=../paperful-data PAPERFUL_OUT=../paperful-out
mkdir -p ../paperful-data/packs ../paperful-data/profiles ../paperful-data/state
mkdir -p ../paperful-out
cp config.minimal.toml ../paperful-data/config.toml # or config.example.toml for the full file
# edit email / ezproxy_base / grey_playbooks_dir = "packs" as needed
# optional: move existing state/ into ../paperful-data/ and out/ into ../paperful-out/
docker compose build
docker compose run --rm paperful doctor
PAPERFUL_DATA / PAPERFUL_OUT in .env default to ../paperful-data and
../paperful-out. Compose mounts them at /data and /data/out. Use
relative out_dir / state_dir / grey_playbooks_dir in config
(e.g. "out", "state", "packs") so they resolve under the mounted data
dir. Absolute host paths (e.g. /Users/...) will not land on the volumes.
Environment and override¶
File |
Role |
|---|---|
Copy to |
|
Default service: |
|
Optional local Compose tweaks |
.env and compose.override.yaml are gitignored so your machine-local paths
never land in the repo. Set PAPERFUL_DATA=. and PAPERFUL_OUT=./out to keep
config/out/state in the repo; defaults keep data and the PDF mirror
outside the git root as sibling dirs.
Image mode: light vs heavy¶
Mode |
|
Python extras |
Typical use |
|---|---|---|---|
light |
|
|
CI, small stranger image |
heavy |
|
|
Full local env |
rag-docling (Torch) is not in either image; use host uv sync --extra rag-docling if you need Docling parsing.
# Local full pack (recommended after cp .env.example .env):
PAPERFUL_IMAGE_MODE=heavy docker compose build
# or: make docker-build-heavy
# Slim pack (CI / default when the variable is unset):
PAPERFUL_IMAGE_MODE=light docker compose build
Rebuild after changing the mode (docker compose build). The image label
paperful.image.mode records which pack was built.
PAPERFUL_ZOTERO_HOST and PAPERFUL_OLLAMA_HOST default to
host.docker.internal so Docker Desktop (macOS/Windows) can reach Zotero and
Ollama on the host. Compose also adds
extra_hosts: host.docker.internal:host-gateway for Linux Docker Engine.
Paperful always sends Host: localhost:23119 — Zotero’s local API requires
that header even when the TCP peer is host.docker.internal. Outside Docker,
host.docker.internal in those env vars falls back to localhost / 127.0.0.1
so a shared .env does not break host uv run.
Optional LLM, RAG, and browser-agent¶
light ships [serve] only (FastAPI/Jinja for compose up).
heavy also installs [llm], [rag] (LanceDB), and [browser-agent]
(browser-use), so rag / ask, LiteLLM, and the browser_agent lane work
inside the container. Headed session login still stays on the host (shared
state/ vault).
Optional GROBID sidecar¶
The opt-in grobid client talks to an external
GROBID REST server. Paperful does not
install Java or GROBID models in the light/heavy images. Run the server on the
host (docker run -p 8070:8070 lfoppiano/grobid:0.8.2) or a second Compose
service via compose.override.example.yaml
and point [grobid].base_url at it (for example
http://host.docker.internal:8070 from a container, or http://grobid:8070
on the Compose network). Keep loopback in config.toml; Paperful rewrites it
via PAPERFUL_GROBID_HOST. Plan for several GB of RAM. Scans need
paperful ocr first. critique --lane marg reuses the same TEI cache for
PDF chunks.
Optional Camofox sidecar¶
The opt-in camofox lane talks to an external
camofox-browser REST server. Paperful
does not install Node or Camoufox in the light/heavy images. Run the server on
the host (or a second Compose service) and point [camofox].base_url at it
(for example http://host.docker.internal:9377 from a container). Set
CAMOFOX_ACCESS_KEY in both places when the server requires a bearer. This is
not a Cloudflare-bypass product and does not reuse the vault Chromium profile.
After doctor is green, docker compose up starts the GUI. It binds in the
container on 0.0.0.0:8765 (Compose passes --allow-remote-bind; the
workbench has no HTTP auth unless PAPERFUL_UI_TOKEN is set) and publishes only 127.0.0.1:8765:8765
(no LAN). Same workbench as host paperful serve (gui.md — Wanted,
then Discover; Advanced Repair / Mirror / Index / Briefs). Operator threat
model: SECURITY.md.
docker compose up
# → http://127.0.0.1:8765
Host-only uv run paperful serve remains the contributor path. fix-metadata title
proposals, the lint identity check, summarize, and synthesize work from the container
against an Ollama running on the host. Keep loopback in config.toml
(base_url = "http://127.0.0.1:11434"); Paperful rewrites that loopback host to
host.docker.internal inside the container via PAPERFUL_OLLAMA_HOST.
A remote base_url stays as configured.
On Docker Desktop, the default Ollama loopback bind is enough (same as
Zotero). On Linux Docker Engine, bind with
OLLAMA_HOST=0.0.0.0 ollama serve so host-gateway can connect.
docker compose run --rm paperful doctor shows the LLM row. Details:
LLM. See rag.md for the index.
Custom playbook packs¶
Put extra grey-playbook TOML files in packs/ (under the data dir) and set in
config. Optional energy and international-org copies (IEA/IRENA,
OECD/WHO/UNEP/UNDP) ship inside the package as
paperful/data/grey_playbooks_examples/. Copy those files into packs/.
That examples folder does not include the ocean builtin, which is already on
by default.
grey_playbooks_dir = "packs"
Merge order: builtin pack → packs/*.toml → inline [[grey_playbooks]]
(same name wins later). See Configuration.
Vault and browser-agent PDF successes append state/fetch-wins.jsonl on this
volume (agent rows may include steps and promotable click: / rewrite
wins). paperful playbooks promote (and [playbooks].promote = "auto")
writes packs/learned.toml when grey_playbooks_dir = "packs". Both state/
and packs/ are gitignored. auto may promote a fluke; the default is
gated. Headed session login stays on the host; a container run still
writes wins onto the shared state/ mount.
Run configs¶
profiles/ next to config.toml holds named run configs (paperful all --profile, paperful profile save). That directory is not the grey-lit
packs/ folder and not state/packs/.
mkdir -p ../paperful-data/profiles
docker compose run --rm paperful profile list
docker compose run --rm paperful all --profile bbnj-journal --dry-run
See Workflows.
After doctor is green¶
Keep Zotero running. docker compose up starts the GUI at
http://127.0.0.1:8765. CLI one-shots use compose run (pass the subcommand —
bare run would also start serve):
docker compose up # stays up; GUI
# other terminal:
docker compose run --rm paperful doctor
docker compose run --rm paperful collections
docker compose run --rm paperful run --collection interesting --dry-run
docker compose run --rm paperful run --collection interesting
docker compose run --rm paperful report
docker compose stop (or Ctrl-C on up) gives the container 30 s
(stop_grace_period) to finish the job it is writing before it is killed;
a Grab interrupted after that is resumable like any run.
--no-attach writes to out/ only. --library walks the whole library
(resumable; Ctrl-C then rerun). Same flags as Commands,
including --year-from / --year-to and --type / -T.
docker compose run --rm paperful run -C BBNJ --year-from 2023 -T journalArticle --dry-run
Common commands¶
docker compose up # stays up; GUI at http://127.0.0.1:8765
# other terminal:
docker compose run --rm paperful doctor
docker compose run --rm paperful doctor --no-guide
docker compose run --rm paperful collections
docker compose run --rm paperful dedupe -C BBNJ --dry-run
docker compose run --rm paperful run --collection interesting --dry-run
docker compose run --rm paperful run --collection interesting
docker compose run --rm paperful report
make docker-build # uses PAPERFUL_IMAGE_MODE from .env
make docker-build-heavy # force heavy pack
make docker-doctor # doctor --guide (interactive Enter walk)
Pass any CLI flag after the service name; the image ENTRYPOINT is paperful.
The same commands as uv snippets — docker compose run --rm paperful
instead of uv run paperful. Headed session login is still host-only.
Image contents¶
Python 3.12, Paperful + Playwright Chromium (htmlpdf / session vault reuse)
Poppler (
pdftotext), OCRmyPDF, Tesseract (eng)[serve]always; heavy also[llm][rag][browser-agent]Vale is not in the image.
doctorambers whenvaleis missing; install it on the host (or a custom image) if you wantcritique --valeNon-root user
paperful(uid 1000)
If bind-mounted out/ / state/ are not writable, fix ownership on the host
(chown -R 1000:1000 …) or run with a matching user override.
Sessions and attach¶
Headed Chromium login and Zotero’s authorize dialog need the host GUI. Typical flow:
docker compose run --rm paperful doctor— prints fix steps for amber session checks. Runpaperful session login ezproxy/scholaron the host (samePAPERFUL_DATA/state/the container mounts), then re-run doctor. For Enter-to-re-check:doctor --guide(usecompose run, notcompose up).Approve attach once on the host so
state/zotero-local-api-key.jsonexists.Run fetch/attach from the container as above.
Usual path (uv)¶
uv sync --group dev
uv run pytest
uv run paperful doctor
See Commands, Architecture, and Zotero.