featherdrop is a sleek, feather-light, self-hosted drop zone for your files — drop a file, set how long it lives (plus an optional password or download limit), and share a short link or QR code. Encrypted at rest, resumable uploads, one small container. No accounts, no clouds, no tracking, no nonsense.
- What is this?
- How it works
- Security & Privacy
- Languages
- Quick Start on Unraid
- Configuration
- Reverse Proxy
- Local Development
- Screenshots
- Contributing / License
- Support this project
featherdrop is a sleek, feather-light, self-hosted file-sharing page for your own server — a much simpler take inspired by Pingvin Share. Where Pingvin ships a full backend, database, and accounts, featherdrop is a single container with no login and no separate database:
- Open the page → a central drop zone is right there.
- Drop a file → a settings panel slides in (expiry, optional password, optional download limit), and a progress ring overlays the drop zone while it uploads.
- You get a shareable link plus a QR code you can save as a PNG. The recipient downloads it any time until it expires.
- Pasting the link into a chat shows a clean preview card — your branding, never the file's name.
- A light/dark toggle and a flag language picker sit in the header — the UI speaks 26 languages and picks yours from the browser.
Highlights
- 🔒 Encrypted at rest with age; the filename
and type are encrypted inside the file. Optional password shares are
end-to-end, and a
MASTER_KEYgives short links. - ⏳ Self-destructing — expiry from 1 hour to 30 days (or never), plus an optional burn-after-N-downloads.
- 🖼️ Inline image/PDF preview and a savable QR code on the share page, with clean link previews that never leak the file's name.
- 🌍 26 languages (right-to-left for Arabic & Hebrew), light/dark, and custom branding (name, logo, accent colour) via env vars.
- 📦 One container — resumable uploads (tus), a single SQLite file, separate data/config volumes, multi-arch (amd64 + arm64).
- 🧹 Private by design — no accounts, no telemetry, no third-party calls at runtime; your files stay on your server.
What it deliberately does not have: user accounts, OIDC/LDAP, email, malware scanning, S3 backends. If you need those, use Pingvin Share — that is the point.
Browser (drop zone, Mantine UI)
│ resumable upload (tus)
▼
featherdrop container (Next.js + small custom Node server)
├─ /files tus upload endpoint
├─ /api/finalize move file into store, write metadata, mint share slug
├─ /d/<slug> share page (info, password gate, download)
└─ cleanup job deletes expired files
▼
/data volume (uploads, bulk) /config volume (metadata, small)
├─ uploads/<id> the files └─ db.sqlite (better-sqlite3 — a file, not a server)
└─ tmp/<id> in-progress uploads
/data (bulk files) and /config (the small SQLite database) are separate
volumes, so you can keep uploads on array storage and the database on a fast
SSD. CONFIG_DIR defaults to DATA_DIR, so a single-volume setup still works.
Uploads are resumable: a dropped connection on a multi-GB transfer resumes instead of starting over. Passwords are scrypt-hashed, never stored in plain text, and large downloads stream natively (no in-browser buffering).
featherdrop is built to be self-hosted: your files and their metadata live only on your server, and the app talks to nobody else.
- No accounts, no tracking. No sign-up, no analytics, no telemetry, and no third-party scripts or fonts pulled at runtime — nothing phones home.
- Your data stays yours. Uploads sit on your
/datavolume, metadata in a local SQLite file. Nothing is ever sent to a cloud or external service. - Encrypted at rest by default with age — the original filename and type are encrypted inside the file, so a stolen disk or backup reveals neither the contents nor the names (details below).
- Optional password protection is end-to-end: even you, the operator, cannot read a password-protected share without the password.
- Self-destructing. Every share has an expiry (down to 1 hour), and an optional download limit burns the file the moment it's reached.
- Minimal attack surface. No login to brute-force, no user database to leak; share slugs are unguessable, and share pages and link previews never expose the file's name.
Provided under the MIT licence without warranty — you run it, you own the data and the responsibility. Put it behind HTTPS (see Reverse Proxy) and, if you set a
MASTER_KEY, keep it safe.
Every uploaded file is encrypted at rest by default, using age — a modern, audited, streaming authenticated encryption format. Each file gets its own key, and the original filename and type are encrypted inside the file, so a stolen disk or backup reveals neither the contents nor the names.
How the per-file key is protected depends on whether you set a password, and on whether you've configured a master key:
| Share type | Where the key lives | Link | What the server can decrypt |
|---|---|---|---|
| Password | Wrapped with your password (age scrypt) | …/d/<slug> |
Nothing without the password — not even the operator |
Server (no password, MASTER_KEY set) |
Wrapped with the server master key | …/d/<slug> — short |
The file (it holds the master key); a stolen data backup alone cannot |
| Link (no password, no master key) | In the share link's #fragment |
…/d/<slug>#k=… — long |
Nothing from the database alone; the key never reaches the server |
Short links. By default a password-less share carries its key in the URL
#fragment, which makes the link long but means the server can never decrypt it.
If you'd rather have short links (…/d/aB3xK), set a MASTER_KEY (see
Configuration): password-less files are then wrapped with it
and stored. The trade-off: the running server can decrypt those files — but a
stolen /data backup still can't, because the master key lives only in the
container environment, not in the volume. Keep it secret and back it up —
losing it makes password-less files unrecoverable. Password shares are
unaffected and stay end-to-end.
Because the key in a link share lives in the URL fragment, it is never sent in an HTTP request and never appears in server logs or your reverse proxy. Treat the full link as the secret: anyone who has it can download the file until it expires.
Encryption streams (age's 64 KiB authenticated chunks), so multi-GB files are
never buffered in memory. Set ENCRYPT_UPLOADS=false to store new uploads as
plaintext if you ever need to; files keep the mode they were stored with.
featherdrop's interface ships in 26 languages. On a visitor's first load the language is taken from their browser; a flag picker beside the light/dark toggle (in the header and on the download page) switches it, and the choice is remembered. Detection runs on the server, so the page is already translated before any JavaScript loads. Arabic and Hebrew render right-to-left.
🇬🇧 English · 🇩🇪 Deutsch · 🇫🇷 Français · 🇪🇸 Español · 🇮🇹 Italiano · 🇵🇹 Português · 🇳🇱 Nederlands · 🇵🇱 Polski · 🇷🇺 Русский · 🇺🇦 Українська · 🇨🇿 Čeština · 🇸🇪 Svenska · 🇩🇰 Dansk · 🇫🇮 Suomi · 🇳🇴 Norsk · 🇹🇷 Türkçe · 🇬🇷 Ελληνικά · 🇭🇺 Magyar · 🇷🇴 Română · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇨🇳 中文 · 🇸🇦 العربية · 🇮🇱 עברית · 🇹🇭 ไทย · 🇻🇳 Tiếng Việt
Each language is a typed file under lib/i18n/locales/, with English as the
source of truth. A native-speaker correction is a one-file edit — pull requests
welcome.
Pull the template into Unraid via the console / SSH:
mkdir -p /boot/config/plugins/dockerMan/templates-user && \
curl -fsSL -o /boot/config/plugins/dockerMan/templates-user/my-featherdrop.xml \
https://raw.githubusercontent.com/junkerderprovinz/unraid-docker-templates/main/featherdrop/featherdrop.xmlThen Docker → Add Container → featherdrop under User templates. Map the Data Directory (uploads) and Config Directory (the database) to your appdata, pick a port, hit Apply, open the WebUI.
The template filename must keep the my- prefix (my-featherdrop.xml) so
Unraid treats it as a user template.
docker run -d \
--name featherdrop \
--restart unless-stopped \
-p 3000:3000 \
-e BASE_URL=https://share.yourdomain.tld \
-e CONFIG_DIR=/config \
-v /mnt/user/appdata/featherdrop/data:/data \
-v /mnt/user/appdata/featherdrop/config:/config \
junkerderprovinz/featherdrop:latestTo keep everything on a single volume instead, drop the CONFIG_DIR line and
the /config mount and map just -v …:/data — the database then lives in
/data alongside the uploads.
| Variable | Default | Description |
|---|---|---|
BASE_URL |
(empty) | Public URL featherdrop is reached at, so share links use your domain. Empty = use the address the browser is on. |
DEFAULT_EXPIRY |
7d |
Expiry pre-selected in the UI. One of 1h, 6h, 1d, 7d, 30d, never. |
MAX_FILE_SIZE |
0 |
Max upload size in bytes. 0 = unlimited (disk-limited). E.g. 5368709120 = 5 GB. |
ENCRYPT_UPLOADS |
true |
Encrypt new uploads at rest with age (see Encryption at rest). Set false to store plaintext. Existing files keep their stored mode. |
MASTER_KEY |
(empty) | Optional secret that gives short links for password-less shares (see Encryption at rest). Generate with openssl rand -base64 32. Keep it secret, back it up; losing it makes password-less files unrecoverable. Empty = long #key links. |
PORT |
3000 |
Port the server listens on. |
DATA_DIR |
/data |
Where the uploaded files live (bulk). Map this to a volume. |
CONFIG_DIR |
(= DATA_DIR) |
Where the SQLite database lives. Defaults to DATA_DIR (single volume). Set it (the Unraid template uses /config) to keep the small database on a separate, faster volume. |
APP_NAME |
featherdrop |
Custom app name — replaces the wordmark in the header and the browser-tab title. |
APP_LOGO |
(empty) | Custom logo (SVG/PNG) replacing the feather: a public image URL, or a data: URI (e.g. data:image/svg+xml;base64,… — generate with base64 -w0 logo.svg) so you need no hosting or file on disk. Empty = the feather. |
ACCENT_COLOR |
#d4af37 |
A 6-digit hex colour for buttons, the upload ring and accents. Invalid values fall back to the gold. |
featherdrop speaks plain HTTP on PORT; put TLS in front of it (Nginx Proxy
Manager, Caddy, Traefik). Two things matter:
- Set
BASE_URLto your public URL so generated links are correct. Use HTTPS — for link shares the decryption key lives in the URL fragment, and TLS keeps the whole link private in transit. - Allow large request bodies and generous timeouts for big uploads. For Nginx / NPM advanced config:
client_max_body_size 0; # no body-size cap (uploads are chunked anyway)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_request_buffering off; # stream uploads straight throughnpm install
npm run dev # http://localhost:3000, data written to ./dataBuild a production bundle and run it the way the container does:
npm run build
npm run startRun the test suite (pure-logic assertions, no framework needed):
npm testStack: Next.js (App Router) + Mantine v7, a small custom Node server
(custom-server.ts) that mounts the tus handler beside Next, better-sqlite3
for metadata, and react-i18next for the UI languages. Files live under
DATA_DIR (default ./data in dev).
The home page — drop a file to share it.
Light and dark themes, with a flag language picker.
Set an expiry, an optional password, and a download limit before sharing.
Your link is ready — copy it or save the QR code.
What the recipient sees: file info and a download button.
Issues and pull requests welcome: https://github.com/junkerderprovinz/featherdrop/issues
Licensed under the MIT License.
If featherdrop saves you a trip to a third-party file host, consider buying me a coffee:
