https://github.com/daemonless/papra
https://github.com/daemonless/papra
Last synced: 12 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/daemonless/papra
- Owner: daemonless
- License: bsd-2-clause
- Created: 2026-06-11T22:31:18.000Z (17 days ago)
- Default Branch: main
- Last Pushed: 2026-06-12T12:23:37.000Z (16 days ago)
- Last Synced: 2026-06-12T14:17:41.696Z (16 days ago)
- Language: Dockerfile
- Size: 49.8 KB
- Stars: 0
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Papra
[](https://github.com/daemonless/papra/actions)
[](https://github.com/daemonless/papra/commits)
Minimalist self-hosted document management platform (Paperless alternative) on FreeBSD.
| | |
|---|---|
| **Registry** | `ghcr.io/daemonless/papra` |
| **Source** | [https://github.com/papra-hq/papra](https://github.com/papra-hq/papra) |
| **Website** | [https://papra.app](https://papra.app) |
## Version Tags
| Tag | Description | Best For |
| :--- | :--- | :--- |
| `latest` | **Upstream Binary**. Built from official release. | Most users. Matches Linux Docker behavior. |
## Prerequisites
Before deploying, ensure your host environment is ready. See the [Quick Start Guide](https://daemonless.io/guides/quick-start) for host setup instructions.
## Deployment
### Podman Compose
```yaml
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"
restart: unless-stopped
```
### AppJail Director
**.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**:
```yaml
options:
- virtualnet: ': 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**:
```
ARG tag=latest
OPTION overwrite=force
OPTION from=ghcr.io/daemonless/papra:${tag}
```
### Podman CLI
```bash
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
```
### Ansible
```yaml
- 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"
```
## 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.0
---
Need help? Join our [Discord](https://discord.gg/Kb9tkhecZT) community.