Author: Dr. Henning Dickten (@hensing)
A lightweight, secure, and Dockerized bridge designed for Raspberry Pi and generic Linux servers. It provides a Samba (SMB) share for hardware document scanners.
Once a scan is saved to the share, this container detects the completed file, uploads it directly to Paperless-ngx via API, and optionally archives or cleans up the local file.
Supports both single-user (simple env var config) and multi-user mode (one share and API key per person).
Single-user mode
graph LR
A[π¨οΈ Hardware Scanner] -- SMB Port 445 --> B(π /data/inbox)
B --> C[π³ Container Watcher]
C -- API Token --> D[π Paperless-NGX]
C -- Move/Delete --> E(π¦ /data/archive)
Multi-user mode
graph TD
SCAN[π¨οΈ Hardware Scanner]
subgraph alice [Alice]
direction TB
AI(π alice_scans)
AW[π³ Watcher]
AA(π¦ archive)
AI --> AW --> AA
end
subgraph bob [Bob]
direction TB
BI(π bob_docs)
BW[π³ Watcher]
BA(π¦ archive)
BI --> BW --> BA
end
PL[π Paperless-NGX]
SCAN -- SMB: alice_scans --> AI
SCAN -- SMB: bob_docs --> BI
AW -- Alice's API Key --> PL
BW -- Bob's API Key --> PL
- π Multi-Arch Support: Optimized for
linux/amd64andlinux/arm64(Raspberry Pi). - π₯ Multi-User Support: Each user gets their own Samba share, SMB credentials, and Paperless-NGX API key.
- π Rootless by Design: Never runs as root, not even briefly at startup β fixed UID/GID 65532:65532 (the "distroless nonroot" convention, deliberately not 1000, to avoid colliding with a real host login user) baked into the image. Port 445 is remapped from an unprivileged internal port via Docker's own port publishing, so the container needs zero Linux capabilities.
- π Samba Integration: Built-in SMB server compliant with modern scanners.
- β‘ Smart Detection: Uses
inotifyto detectclose_writeevents (prevents processing incomplete files). - π·οΈ Auto-Tagging: Automatically apply tags to uploaded documents β configurable per user in multi-user mode.
- π§Ή Auto-Cleanup: Options to archive or delete files after successful upload.
- π‘οΈ SSL Support: Full support for HTTPS and self-signed certificates.
This tool works best as part of a modern document management ecosystem. We highly recommend checking out:
- Paperless-ngx Documentation: The official documentation for the backend system.
- Paperless-GPT: An amazing tool to add AI-powered analysis, tagging, and renaming to your documents after they have been uploaded.
Create your .env file based on the example:
cp .env.example .envMinimal .env example:
PAPERLESS_URL=https://paperless.local:8000
PAPERLESS_API_KEY=your-super-secret-token
SMB_USER=scanner
SMB_PASSWORD=scan123
β οΈ scan123is just an example value, not a real default you should keep. ChangeSMB_PASSWORDto a strong, unique password before starting the container β this share is reachable by anything on your network. The container will log a loud warning at startup for as long as the password is left atscan123.
This container always runs as a fixed UID/GID (65532:65532 by default) and never as root β not even transiently at startup. Before the first start, make sure ./data and ./config are owned by (or writable by) UID/GID 65532 on the host:
mkdir -p data config
chown -R 65532:65532 data configThe container can no longer fix bind-mount ownership for you (that would require a root step, which has been deliberately removed).
Why 65532 and not 1000? Docker doesn't remap container UIDs into a separate namespace by default β "UID 1000 in the container" is literally the same UID as a host account, and on single-user Debian/Raspberry Pi OS installs, UID 1000 is almost always the operator's own (often sudo-capable) login. Since this container runs a network-facing SMB service, a container escape running as that UID would inherit the operator's file access and could potentially piggyback on a still-valid sudo credential cache. 65532 is the widely-used "distroless nonroot" convention β deliberately outside both the Linux system-service range (0β999) and the human-user range (1000+), so it won't collide with a real login account. If you'd rather match your own host user than run the one-time chown (accepting the collision consideration above), rebuild with --build-arg APP_UID=<uid> --build-arg APP_GID=<gid> and update user: in your compose file to match.
Create a docker-compose.yml (or use the one provided):
services:
scan-to-paperless:
image: ghcr.io/hensing/scan-to-paperless:latest
container_name: scan-to-paperless
restart: unless-stopped
user: "65532:65532"
security_opt:
- "no-new-privileges:true"
cap_drop:
- "ALL"
ports:
- "445:8445"
env_file:
- .env
volumes:
- ./data:/data
- ./config:/config:roStart the container:
docker compose up -dConfigure your physical scanner (Brother, Canon, HP, etc.) with these settings:
- Protocol: SMB / CIFS
- Server: IP of your Docker host
- Port: 445
- Share Name:
scanner(default) - Username:
scanner(default) - Password:
scan123(default ββ οΈ example only, change it in.envbefore going live)
Multi-user mode activates automatically when ./config/users.conf exists. Each user gets an isolated Samba share and uploads to Paperless with their own API key.
mkdir -p config
cp config/users.conf.example config/users.confEdit config/users.conf β one user per line, colon-separated:
# smb_user:smb_password:smb_share:paperless_api_key:paperless_tags(optional)
alice:secretpassword1:alice_scans:paperless-api-token-alice:scanned,alice
bob:secretpassword2:bob_docs:paperless-api-token-bob:scanned,bob
Only PAPERLESS_URL and optional processing settings are needed. The SMB_* and PAPERLESS_API_KEY variables are ignored in multi-user mode.
PAPERLESS_URL=https://paperless.local:8000Make sure you've also completed the Host Permissions step (chown -R 65532:65532 data config) β it applies to multi-user mode too.
The image ships with a fixed pool of 32 internal Samba accounts (see config/users.conf.example); rebuild with --build-arg SMB_POOL_SIZE=<N> if you need more than 32 concurrent users.
docker compose up -dEach user's files land in /data/<smb_user>/inbox and are archived to /data/<smb_user>/archive.
Upgrading from single-user: No changes required. Single-user mode is auto-detected when
users.confis absent.
Want John's scans to automatically get shared with the "family" group, or have John's scanner account hand ownership of every document straight to Jane? This container doesn't set Paperless document owner/permissions itself β Paperless-NGX's own Workflows feature already does this well, and reuses code that's actually tested, so let it do the work.
The trick is giving a Workflow something reliable to match on: Paperless's
upload "Source" filter can't tell one API key from another (API and Web-UI
uploads are one bucket), but a tag unique to a user can. That's exactly
what paperless_tags in users.conf is for.
- In Paperless, create a tag that's unique to one user, e.g.
alice_upload. - In
users.conf, add that tag name to the user'spaperless_tagscolumn (comma-separated with any other tags they already get), e.g.:Restart the container after adding a brand-new tag β tag names are resolved to IDs once when each user's watcher starts.alice:secretpassword1:alice_scans:paperless-api-token-alice:scanned,alice_upload - In Paperless, go to Settings β Workflows β add a new workflow:
- Trigger: Document Added
- Condition: has tag(s)
alice_upload - Action(s): Assignment β set the fields you want, e.g. assign owner, or grant view/change permission to specific users or groups.
Some concrete mappings for the scenarios above:
| Goal | Workflow action |
|---|---|
| Auto-share John's uploads (R/W) with the "family" group | Assign view + change permission β group family |
| John scans, ownership goes to Jane | Assign owner β jane |
| A shared scanner account transfers rights on upload | Same as above, keyed off that account's own unique tag |
Since the mapping lives in Paperless's Workflow UI rather than in
users.conf, it can be changed anytime without touching or restarting this
container β only the tag itself needs to exist before the watcher starts.
| Variable | Description | Default | Required |
|---|---|---|---|
PAPERLESS_URL |
Full URL to Paperless-NGX (e.g., http://192.168.1.5:8000) |
- | β |
PAPERLESS_VERIFY_SSL |
Verify SSL certificates (false for self-signed) |
true |
β |
WHITELIST |
Allowed file extensions | pdf,jpg,png,bmp |
β |
ARCHIVE |
true = Move to archive folder, false = Delete after upload |
true |
β |
UPLOAD_TIMEOUT |
Max time (seconds) for API upload | 30 |
β |
SCAN_SETTLE_TIME |
Seconds to wait after detection before upload | 5 |
β |
There is no
PUID/PGIDsetting β the container always runs as fixed UID/GID65532:65532. See Host Permissions.
Ignored when config/users.conf is present.
| Variable | Description | Default |
|---|---|---|
PAPERLESS_API_KEY |
API Token from Paperless Settings β API Tokens | - |
PAPERLESS_TAGS |
Comma-separated tag names (or numeric IDs) to apply β see Automating Ownership & Sharing | "" |
SMB_USER |
Username for the scanner to login | scanner |
SMB_PASSWORD |
Password for the scanner | scan123 |
SMB_SHARE |
Name of the SMB share | scanner |
| Field | Description | Required |
|---|---|---|
smb_user |
Linux/SMB username β unique, no spaces | β |
smb_password |
Password for the SMB share | β |
smb_share |
Share name visible to the scanner | β |
paperless_api_key |
API Token from Paperless Settings β API Tokens | β |
paperless_tags |
Comma-separated tag names or numeric IDs (optional, can be empty) β see Automating Ownership & Sharing | β |
smb_user and smb_share must match ^[A-Za-z][A-Za-z0-9_-]*$; a fixed pool of 32 internal Samba accounts is available by default (SMB_POOL_SIZE build arg) β entries beyond that are skipped with a warning until the image is rebuilt with a larger pool.
Note on Permissions: This container never runs as root, not even transiently at startup. It runs as a fixed UID/GID (
65532:65532by default, overridable at build time via--build-arg APP_UID/APP_GID) baked into the image at build time β there is no runtime PUID/PGID remapping. Because of this, you are responsible for ensuring./dataand./configare owned by (or writable by) that UID/GID before starting the container β the container itself can no longer fix bind-mount ownership, since doing so would require a root step that has been deliberately removed. See Host Permissions.
This version removes the container's root startup phase entirely. If you're upgrading from an older version:
PUID/PGIDare gone. Remove them from your.env. The container now always runs as UID/GID65532:65532.- Rebuild the image (
docker compose build/ re-pull), not just restart β the new image ships a fixed pool of Samba accounts and a non-rootUSERdirective. chown -R 65532:65532 ./data ./configon the host once, before starting β the container no longer fixes bind-mount ownership for you.- Scanner-facing behavior is unchanged (still port
445, sameusers.confformat); no scanner reconfiguration is needed. - Multi-user setups are capped at 32 concurrent users by default; rebuild with
--build-arg SMB_POOL_SIZE=<N>if you need more.
Single-user mode
/data
βββ inbox/ <-- Scanner saves files here (monitored)
βββ archive/ <-- Processed files are moved here (if ARCHIVE=true)
Multi-user mode
/data
βββ alice/
β βββ inbox/ <-- alice's Samba share (monitored)
β βββ archive/ <-- alice's processed files
βββ bob/
βββ inbox/ <-- bob's Samba share (monitored)
βββ archive/ <-- bob's processed files
π "Upload failed" in logs
- Check if
PAPERLESS_URLis reachable from inside the container. - Verify the API key (
PAPERLESS_API_KEYor the key inusers.conf). - If using a self-signed cert, try setting
PAPERLESS_VERIFY_SSL=false. - Increase
SCAN_SETTLE_TIME. Some network scanners report "finished" before the file is fully flushed to disk.
π« Scanner cannot connect (Network Error)
- Ensure port 445 is not blocked by a firewall on the host.
- Windows/Mac hosts might use port 445 for their own sharing service. Ensure port 445 is free or use a different external port (note: many scanners hardcode 445).
π File is ignored
- Check the
WHITELISTin.env. - The container waits for the
close_writeevent. Ensure the scanner finishes writing the file completely.
π₯ Multi-user: user cannot connect
- Verify the share name in
users.confmatches what the scanner is configured with. - Check the container logs for
[INIT] Configured N user(s).β if N is 0, the config file has a parsing issue. - Ensure all four required fields (
smb_user:smb_password:smb_share:paperless_api_key) are present.
Contributions, improvements, and bug fixes are welcome!
- Fork the project.
- Create your feature branch (
git checkout -b feature/AmazingFeature). - Commit your changes (
git commit -m 'Add some AmazingFeature'). - Push to the branch (
git push origin feature/AmazingFeature). - Open a Pull Request.
Note to Forks: Please ensure that the original author credit remains intact in the license and documentation when forking or redistributing this project.
Build the image locally:
docker build -t scan-to-paperless .Distributed under the MIT License. See LICENSE for more information.