Skip to content

Repository files navigation

Papra

Build Status Last Commit

Minimalist self-hosted document management platform (Paperless alternative) on FreeBSD.

Registry ghcr.io/daemonless/papra
Source https://github.com/papra-hq/papra
Website https://papra.app

Version Tags

Tag Description Best For
latest Upstream Binary. Built from official release. Most users — recommended.

Prerequisites

Before deploying, ensure your host environment is ready. See the Quick Start Guide for host setup instructions.

Deployment

Podman Compose

services:
  papra:
    image: "ghcr.io/daemonless/papra:latest"
    container_name: papra
    environment:
      - PUID=1000  # User ID for the application process
      - PGID=1000  # Group ID for the application process
      - TZ=Etc/UTC  # Timezone for the container
      - NODE_ENV=production  # Node runtime mode; leave as 'production'
      - PORT=1222  # Internal node backend port (nginx proxies to it); leave as 1222
      - SERVER_HOSTNAME=127.0.0.1  # Internal bind address for the node backend; leave as 127.0.0.1
      - SERVER_SERVE_PUBLIC_DIR=false  # Whether the node backend serves the SPA itself; 'false' (nginx serves it)
      - DATABASE_URL=file:/app_data/db/db.sqlite  # SQLite database URL (file:/app_data/db/db.sqlite)
      - DOCUMENT_STORAGE_FILESYSTEM_ROOT=/app_data/documents  # Filesystem path where uploaded documents are stored (under the /app_data volume)
      - PAPRA_CONFIG_DIR=/app_data  # Directory Papra reads its config from (under the /app_data volume)
      - INGESTION_FOLDER_ROOT=/ingestion  # Watched folder for drop-in document ingestion
      - EMAILS_DRY_RUN=true  # If 'true', emails are logged instead of sent (no SMTP configured by default)
      - BETTER_AUTH_TELEMETRY=0  # better-auth telemetry; '0' disables it
      - AUTH_SECRET=${PAPRA_AUTH_SECRET}  # better-auth session signing secret, >=32 chars. Optional: if unset, the container generates a strong secret on first boot and persists it under /app_data. Set one you control with `openssl rand -hex 48` to manage it yourself.
      - AUTH_IS_REGISTRATION_ENABLED=true  # Set to false after creating your account to lock down signups
    volumes:
      - "/path/to/containers/papra/app_data:/app_data"
    # always (not unless-stopped) so FreeBSD's podman rc.d auto-starts it at boot
    restart: always

Save as compose.yaml, then run podman-compose up -d.

AppJail Director

.env:

# .env

DIRECTOR_PROJECT=papra
PUID=1000
PGID=1000
TZ=Etc/UTC
NODE_ENV=production
PORT=1222
SERVER_HOSTNAME=127.0.0.1
SERVER_SERVE_PUBLIC_DIR=false
DATABASE_URL=file:/app_data/db/db.sqlite
DOCUMENT_STORAGE_FILESYSTEM_ROOT=/app_data/documents
PAPRA_CONFIG_DIR=/app_data
INGESTION_FOLDER_ROOT=/ingestion
EMAILS_DRY_RUN=true
BETTER_AUTH_TELEMETRY=0
AUTH_SECRET=${PAPRA_AUTH_SECRET}
AUTH_IS_REGISTRATION_ENABLED=true

appjail-director.yml:

# appjail-director.yml

options:
  - virtualnet: ':<random> default'
  - nat:
services:
  papra:
    name: papra
    options:
      - container: 'boot args:--pull'
    oci:
      user: root
      environment:
        - PUID: !ENV '${PUID}'
        - PGID: !ENV '${PGID}'
        - TZ: !ENV '${TZ}'
        - NODE_ENV: !ENV '${NODE_ENV}'
        - PORT: !ENV '${PORT}'
        - SERVER_HOSTNAME: !ENV '${SERVER_HOSTNAME}'
        - SERVER_SERVE_PUBLIC_DIR: !ENV '${SERVER_SERVE_PUBLIC_DIR}'
        - DATABASE_URL: !ENV '${DATABASE_URL}'
        - DOCUMENT_STORAGE_FILESYSTEM_ROOT: !ENV '${DOCUMENT_STORAGE_FILESYSTEM_ROOT}'
        - PAPRA_CONFIG_DIR: !ENV '${PAPRA_CONFIG_DIR}'
        - INGESTION_FOLDER_ROOT: !ENV '${INGESTION_FOLDER_ROOT}'
        - EMAILS_DRY_RUN: !ENV '${EMAILS_DRY_RUN}'
        - BETTER_AUTH_TELEMETRY: !ENV '${BETTER_AUTH_TELEMETRY}'
        - AUTH_SECRET: !ENV '${AUTH_SECRET}'
        - AUTH_IS_REGISTRATION_ENABLED: !ENV '${AUTH_IS_REGISTRATION_ENABLED}'
    volumes:
      - papra_app_data: /app_data
volumes:
  papra_app_data:
    device: '/path/to/containers/papra/app_data'

Makejail:

# Makejail

ARG tag=latest

OPTION overwrite=force
OPTION from=ghcr.io/daemonless/papra:${tag}

Save the files above, then run appjail-director up.

Podman CLI

podman run -d --name papra \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Etc/UTC \
  -e NODE_ENV=production \
  -e PORT=1222 \
  -e SERVER_HOSTNAME=127.0.0.1 \
  -e SERVER_SERVE_PUBLIC_DIR=false \
  -e DATABASE_URL=file:/app_data/db/db.sqlite \
  -e DOCUMENT_STORAGE_FILESYSTEM_ROOT=/app_data/documents \
  -e PAPRA_CONFIG_DIR=/app_data \
  -e INGESTION_FOLDER_ROOT=/ingestion \
  -e EMAILS_DRY_RUN=true \
  -e BETTER_AUTH_TELEMETRY=0 \
  -e AUTH_SECRET=${PAPRA_AUTH_SECRET} \
  -e AUTH_IS_REGISTRATION_ENABLED=true \
  -v /path/to/containers/papra/app_data:/app_data \
  ghcr.io/daemonless/papra:latest

Save as run.sh, then run sh run.sh.

AppJail

appjail oci run -Pd \
  -o overwrite=force \
  -o container="args:--pull" \
  -o virtualnet=":<random> default" \
  -o nat \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Etc/UTC \
  -e NODE_ENV=production \
  -e PORT=1222 \
  -e SERVER_HOSTNAME=127.0.0.1 \
  -e SERVER_SERVE_PUBLIC_DIR=false \
  -e DATABASE_URL=file:/app_data/db/db.sqlite \
  -e DOCUMENT_STORAGE_FILESYSTEM_ROOT=/app_data/documents \
  -e PAPRA_CONFIG_DIR=/app_data \
  -e INGESTION_FOLDER_ROOT=/ingestion \
  -e EMAILS_DRY_RUN=true \
  -e BETTER_AUTH_TELEMETRY=0 \
  -e AUTH_SECRET=${PAPRA_AUTH_SECRET} \
  -e AUTH_IS_REGISTRATION_ENABLED=true \
  -o fstab="/path/to/containers/papra/app_data /app_data <pseudofs>" \
  ghcr.io/daemonless/papra:latest papra

Save as run.sh, then run sh run.sh.

Bastille

Warning

Bastille's OCI support is experimental. It requires buildah, shares the host network stack (inherit), and persists image-declared volumes under --data-path.

services:
  papra:
    image: "ghcr.io/daemonless/papra:latest"
    container_name: papra
    network_mode: host  # jail shares host networking
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
      - NODE_ENV=production
      - PORT=1222
      - SERVER_HOSTNAME=127.0.0.1
      - SERVER_SERVE_PUBLIC_DIR=false
      - DATABASE_URL=file:/app_data/db/db.sqlite
      - DOCUMENT_STORAGE_FILESYSTEM_ROOT=/app_data/documents
      - PAPRA_CONFIG_DIR=/app_data
      - INGESTION_FOLDER_ROOT=/ingestion
      - EMAILS_DRY_RUN=true
      - BETTER_AUTH_TELEMETRY=0
      - AUTH_SECRET=${PAPRA_AUTH_SECRET}
      - AUTH_IS_REGISTRATION_ENABLED=true

Save as podman-compose.yml, then run bastille up. Or via CLI:

bastille create -O \
  --env PUID=1000 \
  --env PGID=1000 \
  --env TZ=Etc/UTC \
  --env NODE_ENV=production \
  --env PORT=1222 \
  --env SERVER_HOSTNAME=127.0.0.1 \
  --env SERVER_SERVE_PUBLIC_DIR=false \
  --env DATABASE_URL=file:/app_data/db/db.sqlite \
  --env DOCUMENT_STORAGE_FILESYSTEM_ROOT=/app_data/documents \
  --env PAPRA_CONFIG_DIR=/app_data \
  --env INGESTION_FOLDER_ROOT=/ingestion \
  --env EMAILS_DRY_RUN=true \
  --env BETTER_AUTH_TELEMETRY=0 \
  --env AUTH_SECRET=${PAPRA_AUTH_SECRET} \
  --env AUTH_IS_REGISTRATION_ENABLED=true \
  --data-path /path/to/containers/papra \
  papra ghcr.io/daemonless/papra:latest inherit

Ansible

- name: Deploy papra
  containers.podman.podman_container:
    name: papra
    image: "ghcr.io/daemonless/papra:latest"
    state: started
    restart_policy: always
    env:
      PUID: "1000"
      PGID: "1000"
      TZ: "Etc/UTC"
      NODE_ENV: "production"
      PORT: "1222"
      SERVER_HOSTNAME: "127.0.0.1"
      SERVER_SERVE_PUBLIC_DIR: "false"
      DATABASE_URL: "file:/app_data/db/db.sqlite"
      DOCUMENT_STORAGE_FILESYSTEM_ROOT: "/app_data/documents"
      PAPRA_CONFIG_DIR: "/app_data"
      INGESTION_FOLDER_ROOT: "/ingestion"
      EMAILS_DRY_RUN: "true"
      BETTER_AUTH_TELEMETRY: "0"
      AUTH_SECRET: "${PAPRA_AUTH_SECRET}"
      AUTH_IS_REGISTRATION_ENABLED: "true"
    volumes:
      - "/path/to/containers/papra/app_data:/app_data"

Save as papra-deploy.yaml, then run ansible-playbook papra-deploy.yaml.

Parameters

Environment Variables

Variable Default Description
PUID 1000 User ID for the application process
PGID 1000 Group ID for the application process
TZ Etc/UTC Timezone for the container
NODE_ENV production Node runtime mode; leave as 'production'
PORT 1222 Internal node backend port (nginx proxies to it); leave as 1222
SERVER_HOSTNAME 127.0.0.1 Internal bind address for the node backend; leave as 127.0.0.1
SERVER_SERVE_PUBLIC_DIR false Whether the node backend serves the SPA itself; 'false' (nginx serves it)
DATABASE_URL file:/app_data/db/db.sqlite SQLite database URL (file:/app_data/db/db.sqlite)
DOCUMENT_STORAGE_FILESYSTEM_ROOT /app_data/documents Filesystem path where uploaded documents are stored (under the /app_data volume)
PAPRA_CONFIG_DIR /app_data Directory Papra reads its config from (under the /app_data volume)
INGESTION_FOLDER_ROOT /ingestion Watched folder for drop-in document ingestion
EMAILS_DRY_RUN true If 'true', emails are logged instead of sent (no SMTP configured by default)
BETTER_AUTH_TELEMETRY 0 better-auth telemetry; '0' disables it
AUTH_SECRET ${PAPRA_AUTH_SECRET} better-auth session signing secret, >=32 chars. Optional: if unset, the container generates a strong secret on first boot and persists it under /app_data. Set one you control with openssl rand -hex 48 to manage it yourself.
AUTH_IS_REGISTRATION_ENABLED true Set to false after creating your account to lock down signups

Volumes

Path Description
/app_data Application data — SQLite database, stored documents, and config

Architectures: amd64 User: bsd (UID/GID via PUID/PGID, defaults to 1000:1000) Base: FreeBSD 15


Need help? Join our Discord community.

About

Minimalist self-hosted document management platform (Paperless alternative) on FreeBSD.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages