Installation
Paperless Starfruit ships as one container: a Node process plus a volume for its SQLite database. No Postgres, no Redis. Pre-built images are published to GitHub Container Registry, so there is nothing to clone or build:
ghcr.io/sabbaken/paperless-starfruit/api:latest # or a release version, e.g. :1.1.0, to pinAll you need is Docker with Compose v2. Provider keys can be added later from the UI.
Deploy
Section titled “Deploy”Two compose files, depending on where you start:
Save as docker-compose.yml (this is docker/docker-compose.yml
from the repo, shown verbatim):
# Standalone compose for Paperless Starfruit: a single container, no external# services. Uses the pre-built image from ghcr.io, so this file works on its# own: no repo checkout or local build needed. The SQLite DB lives on the named# volume; the paperless connection and provider keys are configured from the# web UI (not here).## ENCRYPTION_KEY is the ONE required secret: it encrypts stored credentials and# signs admin sessions, so it must be set and kept stable (rotating it logs the# admin out and makes saved credentials undecryptable). Provide it via the shell# or a `.env` file next to this compose file. Generate one with:# openssl rand -base64 32services: paperless-starfruit: # Published by CI on every release (root package.json version bump). Tags: # `latest`, or the release version (e.g. `1.1.0`) to pin. To build from a # repo checkout instead, replace `image:` with: # build: # context: .. # dockerfile: docker/Dockerfile image: ghcr.io/sabbaken/paperless-starfruit/api:latest labels: # Opt this container in to the Watchtower auto-updater defined below. # Remove this label to keep the container on its current image. - 'com.centurylinklabs.watchtower.enable=true' ports: # Host port 7827 ("star" on a phone keypad), deliberately uncommon so # this file works as-is, unlike 3000 which is usually taken. The app # inside the container still listens on 3000. - '7827:3000' volumes: - paperless_starfruit_data:/data environment: DATABASE_PATH: /data/app.db ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY must be set, see the comment above} # Optional: lock CORS to specific origins (comma-separated). Unset reflects # the request origin, which is fine since auth is a bearer token, not a cookie. # CORS_ORIGIN: https://paperless-ai.home.lan restart: unless-stopped
# Auto-updates (optional). Watchtower watches the registry and, when a newer # image is published, pulls it and recreates the container with the same # config. `--label-enable` means it only touches containers carrying the # watchtower label above, so nothing else on the host (e.g. your existing # paperless-ngx stack) is affected. Delete this whole service to opt out. watchtower: image: nickfedor/watchtower:latest # maintained fork (containrrr/watchtower is archived) restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock command: --label-enable --cleanup --interval 300 # --label-enable only update containers with the enable label (see above) # --cleanup delete the old image after a successful update # --interval 300 check the registry every 5 min (seconds; raise for fewer checks)
# To run alongside an existing paperless-ngx stack, attach this service to that# stack's Docker network so it can reach paperless by its service name. Find the# network with `docker network ls` (usually "<paperless-project>_default"), then:## services:# paperless-starfruit:# networks: [default, paperless]# networks:# paperless:# external: true# name: paperless_default # <- your paperless stack's network## Then in the web UI set the paperless URL to the in-network service name, e.g.# http://webserver:8000, NOT http://localhost:8000 (localhost is this container).
volumes: paperless_starfruit_data:Put the one required secret in a .env next to it and start:
echo "ENCRYPTION_KEY=$(openssl rand -base64 32)" > .envdocker compose up -dThe container has to reach paperless over the network:
-
If paperless has a LAN address or published port, just enter that URL in the UI later, e.g.
http://192.168.1.10:8000orhttps://paperless.home.lan. -
To use the Docker service name instead, attach the container to your paperless stack’s network. Find it with
docker network ls(usually<project>_default), then:services:paperless-starfruit:networks: [default, paperless]networks:paperless:external: truename: paperless_default # your paperless stack's networkand use
http://webserver:8000as the paperless URL, notlocalhost, which is this container.
A full stack: Redis + paperless-ngx + Paperless Starfruit. Save as docker-compose.full.yml
(this is docker/docker-compose.full.yml
from the repo, shown verbatim):
# Full-stack example: paperless-ngx + Paperless Starfruit in one stack, using# the pre-built Starfruit image from ghcr.io: no repo checkout or local build# needed, this file works on its own. If you ALREADY run paperless-ngx, use# docker-compose.yml instead and attach it to your existing stack's network.## Put this file on your server with a `.env` next to it holding the two# required secrets (generate each with `openssl rand -base64 32`):## ENCRYPTION_KEY=...# PAPERLESS_SECRET_KEY=...## docker compose -f docker-compose.full.yml up -d# docker compose -f docker-compose.full.yml run --rm paperless createsuperuser## Then:# http://localhost:8000 paperless-ngx (log in with the superuser you created)# http://localhost:7827 Paperless Starfruit (first visit shows "create admin")## In the Starfruit UI, set the paperless URL to http://paperless:8000, the# in-network service name, NOT http://localhost:8000 (inside the Starfruit# container, localhost is the container itself).services: broker: image: docker.io/library/redis:8 restart: unless-stopped volumes: - redisdata:/data
paperless: # Pin a specific version (e.g. :2.18) if you prefer deliberate upgrades. image: ghcr.io/paperless-ngx/paperless-ngx:latest restart: unless-stopped depends_on: - broker ports: - '8000:8000' environment: PAPERLESS_REDIS: redis://broker:6379 PAPERLESS_SECRET_KEY: ${PAPERLESS_SECRET_KEY:?set PAPERLESS_SECRET_KEY in .env (any long random string)} # The URL paperless is reached at in the browser (needed for CSRF). # Change this when serving behind a reverse proxy / real hostname. PAPERLESS_URL: ${PAPERLESS_URL:-http://localhost:8000} PAPERLESS_TIME_ZONE: ${TZ:-UTC} # PAPERLESS_OCR_LANGUAGE: eng # No PAPERLESS_DBHOST -> paperless uses its built-in SQLite database, # plenty for a personal archive. See the paperless-ngx docs to switch to # Postgres. volumes: - paperless_data:/usr/src/paperless/data - paperless_media:/usr/src/paperless/media - ./export:/usr/src/paperless/export # Drop PDFs/images into ./consume (next to this file) to have paperless # ingest them. - ./consume:/usr/src/paperless/consume
paperless-starfruit: # Published by CI on every release (root package.json version bump). Tags: # `latest`, or the release version (e.g. `1.1.0`) to pin. image: ghcr.io/sabbaken/paperless-starfruit/api:latest restart: unless-stopped labels: # Opt this container in to the Watchtower auto-updater defined below. # Remove this label to keep the container on its current image. - 'com.centurylinklabs.watchtower.enable=true' depends_on: - paperless ports: # Host port 7827 ("star" on a phone keypad), deliberately uncommon so # this file works as-is, unlike 3000 which is usually taken. The app # inside the container still listens on 3000. - '7827:3000' volumes: - starfruit_data:/data environment: DATABASE_PATH: /data/app.db # Encrypts stored credentials and signs admin sessions. Keep it stable: # rotating it logs the admin out and makes saved credentials # undecryptable. ENCRYPTION_KEY: ${ENCRYPTION_KEY:?set ENCRYPTION_KEY in .env (generate with `openssl rand -base64 32`)} # Optional: lock CORS to specific origins (comma-separated). Unset # reflects the request origin, which is fine since auth is a bearer # token, not a cookie. # CORS_ORIGIN: https://paperless-ai.home.lan
# Auto-updates (optional). Watchtower watches the registry and, when a newer # image is published, pulls it and recreates the container with the same # config. `--label-enable` means it only touches containers carrying the # watchtower label above, so paperless, Redis, etc. are left alone and only # Starfruit auto-updates. Delete this whole service to opt out. watchtower: image: nickfedor/watchtower:latest # maintained fork (containrrr/watchtower is archived) restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock command: --label-enable --cleanup --interval 300 # --label-enable only update containers with the enable label (see above) # --cleanup delete the old image after a successful update # --interval 300 check the registry every 5 min (seconds; raise for fewer checks)
volumes: redisdata: paperless_data: paperless_media: starfruit_data:Generate the two secrets, start the stack, and create the paperless superuser:
cat > .env <<EOFENCRYPTION_KEY=$(openssl rand -base64 32)PAPERLESS_SECRET_KEY=$(openssl rand -base64 32)EOF
docker compose -f docker-compose.full.yml up -ddocker compose -f docker-compose.full.yml run --rm paperless createsuperuserpaperless-ngx comes up on http://localhost:8000. Log in with the superuser you just
created. In the Starfruit UI, use http://paperless:8000 as the paperless URL (the
in-network service name, not localhost).
Then open http://localhost:7827. The container serves the API and the web UI on the
same port. On first run the UI shows a “create admin” card; once you set the admin username
and password, registration closes and only login works. There is no default password.
Environment variables
Section titled “Environment variables”These are the only runtime variables. Everything else lives in the UI.
| Variable | Required | Default | Purpose |
|---|---|---|---|
ENCRYPTION_KEY |
yes | (none) | 32-byte key (base64 or hex). Encrypts stored credentials and signs admin sessions. |
DATABASE_PATH |
no | /data/app.db |
SQLite file path. Its directory is created if missing. |
PORT |
no | 3000 |
Port the app listens on inside the container (the compose files publish it as 7827). |
CORS_ORIGIN |
no | reflect origin | Comma-separated allowed origins. Leave unset unless you want to lock it down. |
Operating notes
Section titled “Operating notes”- Persistence. Everything (config, prompts, job queue, review items, audit history) is
one SQLite file at
/data/app.dbon a named volume. Back that volume up and you have a complete snapshot. - Reverse proxy. Auth is a bearer token, not a cookie, so no extra CORS or cookie
configuration is needed. Put your TLS terminator (Caddy, Traefik, nginx) in front of the
published port (
7827by default). - Health.
GET /api/healthbacks the image’sHEALTHCHECK. Migrations apply automatically on boot; there is no separate migrate step. - Automatic updates. The compose files include a Watchtower
service that checks the registry and, when a newer image is published, pulls it and recreates
the Starfruit container in place. It only updates containers carrying the
com.centurylinklabs.watchtower.enable=truelabel, so nothing else on the host is touched. Remove that label (or thewatchtowerservice) to stay on a fixed image, and pin a version tag like:1.1.0instead of:latestif you’d rather upgrade deliberately. - Building from source (optional). Clone the repo and swap
image:for the commentedbuild:block indocker/docker-compose.yml.
