KahWee - Web Development, AI Tools & Tech Trends

Expert takes on AI tools like Claude and Sora, modern web development with React and Vite, and tech trends. By KahWee.

Self-Hosting Papra with Docker Compose on a NAS

Papra is an open-source document management system. The Docker Compose install is four steps. The fifth step, fixing database ownership, is the one that isn't documented and will silently break your setup if you skip it.

Directory Setup

SSH into your NAS and create the working directories:

mkdir -p /volume1/docker/papra/app-data/db
mkdir -p /volume1/documents

The db directory holds the SQLite database. Documents live separately at /volume1/documents so they survive container recreation.

Compose File

Generate an auth secret first:

openssl rand -hex 32

Then write the compose file:

cat > /volume1/docker/papra/docker-compose.yml << 'EOF'
services:
  papra:
    container_name: papra
    image: ghcr.io/papra-hq/papra:latest
    restart: unless-stopped
    ports:
      - "1221:1221"
    environment:
      - AUTH_SECRET=<your-generated-secret>
      - APP_BASE_URL=http://<your-nas-hostname>:1221
    volumes:
      - ./app-data/db:/app/app-data/db
      - /volume1/documents:/app/app-data/documents
    user: "1000:1000"
EOF

Replace <your-generated-secret> and <your-nas-hostname>. Run id -u && id -g to confirm your actual UID and GID before setting the user field — Synology and UGREEN differ.

APP_BASE_URL must match the URL you use in the browser exactly. A mismatch causes an "Invalid application origin" error on the register page.

Start

cd /volume1/docker/papra && docker compose up -d

Papra runs all database migrations automatically on first start. You'll see 18 migration entries in the logs, then Server started on port 1221.

Fix Database Permissions

Caution

Registration fails silently if the SQLite database is owned by root. The error in the logs is SQLITE_READONLY. The error on screen is "Failed to create user" with no explanation.

The first container run may write db.sqlite as root if your UID/GID setup wasn't right. Fix it with a temporary Alpine container — no sudo required:

docker run --rm \
  -v /volume1/docker/papra/app-data:/data \
  alpine chown -R 1000:1000 /data

Then restart:

cd /volume1/docker/papra && docker compose restart

Verify before restarting by checking the file owner:

ls -la /volume1/docker/papra/app-data/db/

The db.sqlite file should be owned by your user, not root.

Register

Go to http://<your-nas-hostname>:1221/register. There are no default credentials. The first account you create is your account.

Upgrading

Papra uses image tags rather than pinned versions, so pulling latest is the upgrade path:

cd /volume1/docker/papra
docker compose pull
docker compose up -d

Compose stops the old container, starts the new one, and migrations run automatically. No manual schema steps. Confirm the new image is running:

docker inspect papra --format '{{.Config.Image}}'
docker logs papra 2>&1 | tail -20

The AUTH_SECRET must stay consistent across upgrades — changing it invalidates all existing sessions.