CastleParcel
Docker app from junkerderprovinz's Repository
Overview
Readme
View on GitHub
Build Lint Docker Pulls Image Size Arch Go Unraid License: AGPL-3.0
CastleParcel is a self-hosted drop zone for your files, with castle walls around it. Think of the site everyone uses to send big files, only on your own server, with no size paywall, and sealed so tight that not even that server can look inside. Drop your files (one or a whole batch), set how long they live (plus an optional password or download limit), and share a short link or QR code. Zero-knowledge end-to-end encryption, resumable uploads, inline previews, one tiny container. No accounts, no clouds, no tracking, no nonsense.
A one-knight job: I build it, keep it running, work through the issues and add what people ask for, until nothing is missing. It is free, with no accounts, no telemetry, no ads and no paid tier. No asterisk anywhere. Nothing readable ever leaves your own walls. Forged on evenings and weekends, with heart and stubbornness.
If it has earned a place on your server or computer, toss a coin to your knight: it helps cover the costs and keeps the project alive. It also makes this knight's heart beat a little faster. Three ways below, whichever suits you.
The files and links in these pictures are made up.
Pick the files, then decide how long they live and how often they can be downloaded
The link is ready: copy it or save the QR code
What the recipient sees, decrypted in their browser
- Zero-knowledge. Files are encrypted in your browser before they upload (libsodium XChaCha20-Poly1305), names included. The key lives in the link's
#fragmentor comes from the share's password, so the server and anyone with its disk only ever see ciphertext. - Shares that clean up after themselves. Expiry from one hour to 30 days or never, an optional download limit and an optional password. A download counts as soon as it starts, and one that breaks off picks up where it stopped, so a shaky connection doesn't waste it. Recipients of a limited link see the file names and sizes before they spend one. A private management link lets you delete a share early. Several files travel as one bundle under one link. The recipient saves them all as one ZIP, built in the browser while it decrypts, and on a share without a download limit can also pick single files.
- Notes and file requests. Share a text note with the same encryption and limits as a file. Or ask someone for files: they get a link to send you files without an account, and only your private collect link can open what arrives.
- Big files are fine. Uploads go in resumable 64 MiB chunks, so a dropped connection picks up where it stopped and a size-capped CDN such as Cloudflare does not get in the way.
- Photos travel light. JPEGs lose their EXIF and GPS data in the browser before encryption, unless you switch it off.
- Pleasant to use. Paste a screenshot with Ctrl+V, install it as an app and share to it from Android, preview images, video, audio, text and PDF before downloading, and read it all in 42 languages.
- Safe to put online. Per-IP rate limits, an optional daily upload cap per visitor, a storage quota, an expiry cap and an optional upload password. A strict Content-Security-Policy keeps foreign scripts off the page, and share pages tell search engines to stay away.
- One small container. A static Go binary on a distroless base with a single SQLite file, for amd64 and arm64, with SBOM and provenance attestations and a CVE scan on every build.
It has no user accounts, no OIDC or LDAP, no email and no S3 storage. If you need those, Pingvin Share is the bigger tool that inspired this one.
Everything is encrypted in your browser, so the server only ever holds ciphertext. That protects a lot, but not everything, and one part still comes down to trust. Here is where the lines are.
The server never sees your files, their names or types, or the password in readable form. The key travels in the # part of the link, and browsers do not send that part to the server. Shares in the v2 format pad their sizes with Padmé, so the server learns a size bucket instead of the exact size or the number of files. Every new share is written in v2. Older shares stay readable, and for those the server sees the exact size, while one protected by a password alone can be guessed offline by anyone who has its slug. docs/encryption-format.md covers both in detail.
Whoever runs the server can still see visitors' IP addresses, when a share was uploaded and downloaded and how often, its expiry and download limit, and the size of the encrypted file. With HTTPS, someone watching the network sees that you talk to the server, when, and roughly how much data moves. Without HTTPS they can also change the page on its way to you, so put HTTPS in front of any instance you use for real. If the operator sets APP_LOGO to an outside URL, that host sees your IP address as well.
The weak spot is the JavaScript. Your browser runs whatever code the server sends, and a server that was broken into, or a tampered image, could send code that reads your key or your files before they are encrypted. It could even do that for a few visitors only. No web app can rule this out by itself, so CastleParcel makes the code checkable instead:
- The client builds to the same bytes on any machine. CI builds it twice on Linux, Windows and macOS and compares the results.
- Every release carries
castleparcel-client.json, with the SHA-256 of every file of the client and one root hash over all of them. - Each instance serves the manifest of the client it embeds at
/.well-known/castleparcel-client.jsonand prints the root hash in its log at startup. index.htmlpins its script and stylesheet with Subresource Integrity, so the browser refuses a file that does not match. The page itself is the release's template with the instance's name filled in.- The crypto worker, which does the encryption, decryption and key derivation of every share format, is part of that pinned script. The page starts it from a
blob:URL made from the script's own code, so there is no separate worker file to check. - The service worker
sw-download.jscannot be pinned that way, and it handles downloads after they are decrypted. It is in the manifest like every other file, so the check below covers it.
To check an instance against a release, run this from a checkout of this repository (Node 20 or newer):
curl -LO https://github.com/junkerderprovinz/castleparcel/releases/download/vX.Y.Z/castleparcel-client.json
node scripts/verify-client.mjs https://share.yourdomain.tld castleparcel-client.json
The script fetches every file, compares its hash with the release and makes sure the page differs from the template only by escaped text where the template has placeholders. For an image you have not started, docker run --rm junkerderprovinz/castleparcel:latest -client-manifest prints the manifest of the client inside it. docs/verifying-the-client.md explains the format and how to rebuild a release yourself.
A check proves what the instance sent to you at that moment. It says nothing about what the server sends to someone else, and your browser does not compare the code with the release on its own.
The full link is the key, so anyone who has it can open the share until it expires or runs out of downloads. A few habits help:
- Send the link over a channel you trust, and the password, if you set one, over a different one.
- A chat or mail service keeps the whole link, key included, in its copy of your message. Browsers keep it in their history and may sync it.
- Pick a short expiry or a download limit, and delete the share with the management link once it has arrived. Keep the management link to yourself.
Some things are out of reach for any file sharer: malware or a hostile browser extension on the sender's or the recipient's device, a recipient who passes the link on, an operator who deletes shares or shuts the server down, and someone who watches the network long enough to match uploads to downloads.
On Unraid, install CastleParcel from Community Applications. Anywhere else:
docker run -d --name castleparcel -p 3000:3000 \
-e BASE_URL=https://share.yourdomain.tld \
-e CONFIG_DIR=/config \
-v /path/to/data:/data \
-v /path/to/config:/config \
junkerderprovinz/castleparcel:latest
Then open port 3000 and drop a file. /data holds the uploads and /config the small database, so the two can sit on different disks. To keep both in /data, set CONFIG_DIR=/data and leave out the /config mount.
Put HTTPS in front of it before you share links outside your network. The browser needs a secure context, HTTPS or localhost, for streaming downloads, resuming an upload that broke off, and the clipboard. Over plain HTTP uploads still work, but one that breaks off starts over. Set BASE_URL to the public address so the links point there, and in Nginx or Nginx Proxy Manager let big uploads through:
client_max_body_size 0;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_request_buffering off;
On an instance anyone can reach, UPLOAD_PASSWORD keeps strangers from uploading while every share link still works for its recipient. Behind a reverse proxy, set TRUST_PROXY to the proxy's address so the rate limits count real visitors instead of the proxy.
Settings
Everything is set through environment variables. A value the server cannot read stops it at boot with a message that names the variable.
| Variable | Default | What it does |
|---|---|---|
BASE_URL |
none | Public address for share links. Without it, links use the address the uploader opened. |
DATA_DIR |
/data |
Where the uploaded files live. |
CONFIG_DIR |
/config |
Where the SQLite database lives. |
PORT |
3000 |
Port the server listens on. |
MAX_FILE_SIZE |
0 |
Largest upload in bytes. 0 means no limit. |
DEFAULT_EXPIRY |
7d |
Expiry picked for a new share: 1h, 6h, 1d, 7d, 30d or never. |
MAX_EXPIRY |
none | Longest expiry an uploader can choose, from the same values. |
STORAGE_QUOTA |
0 |
Bytes all shares together may use, uploads in progress included. 0 means no limit. |
UPLOAD_PASSWORD |
none | Password needed to upload. Downloads never ask for it. |
UPLOAD_LIMIT_PER_IP |
0 |
Bytes one visitor may upload in 24 hours. 0 means no limit. IPv6 visitors count per /64. The count stays in memory under a hash that changes with every restart, so the server never keeps an address. |
RATE_LIMIT |
true |
Per-visitor limits on creating uploads, on new downloads and on failed share lookups. |
TRUST_PROXY |
false |
Which reverse proxies may name the visitor in X-Forwarded-For. true trusts any, or list the proxies as addresses and CIDR ranges, such as 172.16.0.0/12,10.0.0.5. |
APP_NAME |
CastleParcel |
Name shown on the page and in its title. |
APP_LOGO |
none | Logo as an https:// URL or an image data: URL. The page allows images from that URL's host and nothing else from it, and visitors' browsers load the logo from there. |
ACCENT_COLOR |
#d4af37 |
Accent colour as a six-digit hex value. |
Coming from featherdrop
CastleParcel used to be called featherdrop. The share formats, the database and the links stayed the same, so an existing install moves over with all its shares and every link it ever sent keeps working.
- Unraid: edit the featherdrop container, set Repository to
junkerderprovinz/castleparcel:latestand apply. Leave the paths as they are. - docker run: start
junkerderprovinz/castleparcel:latestwith the same/dataand/configfolders. - Compose: change the image to
junkerderprovinz/castleparcel:latest. If you take the new compose.yaml instead, stop the old featherdrop container first, since both want port 3000. The new file uses the same./appdatafolders as the old one.
The featherdrop images stay on Docker Hub and GHCR, but they get no more updates.
Other Docker hosts
compose.yaml runs CastleParcel on any Linux server with Docker. Put it in a folder of its own and start it with docker compose up -d. The folders are created next to the file.
- Synology: in Container Manager, create a project from the file (DSM 7.2 or newer).
- Proxmox: run it in a VM or an LXC container that has Docker.
- MOS: install CastleParcel from the MOS Hub.
- ZimaOS and CasaOS: add the community store and install CastleParcel from it.
- umbrelOS: add the community app store and install CastleParcel from it.
One knight builds this, and AI is one of the tools I work with, the same way I work with an editor or a compiler. It helps me write code and documentation and it checks my work, and that saves me a good many evenings. It does not make the decisions, though. I read and understand everything before it ships, and if something here breaks, that is on me and not on the tool.
You do not have to take my word for it. The code is open and every release note is written by hand. The issue tracker shows how problems actually get handled, including the ones I got wrong the first time. If you find something that is not right, open an issue and I will look at it.
Questions? Check the support thread. Bugs, ideas or feature requests? Please open a GitHub issue.
The name CastleParcel, its logo and its branding are not covered by the AGPL-3.0 licence of the code: a fork needs its own name and look.
The licences of the libraries it ships with: every instance serves those of the web client at /third-party-licenses.md, and the image keeps all of them under /licenses.
Categories
Download Statistics
Related apps
Explore more like this
Explore allLinks
Details
junkerderprovinz/castleparcel:latestRuntime arguments
- Web UI
http://[IP]:[PORT:3000]/- Network
bridge- Shell
bash- Privileged
- false
- Extra Params
--restart unless-stopped
Template configuration
Port the CastleParcel web interface listens on.
- Target
- 3000
- Default
- 3000
- Value
- 3000
Bulk storage for the uploaded files. Can live on slower array storage. Kept separate from the small metadata database (see Config Directory). The folder keeps the featherdrop name so existing shares stay where they are.
- Target
- /data
- Default
- /mnt/user/appdata/featherdrop/data
- Value
- /mnt/user/appdata/featherdrop/data
Stores the small SQLite metadata database (db.sqlite). Best kept on fast storage (e.g. the SSD cache/appdata). Separate from the bulk uploads (see Data Directory).
- Target
- /config
- Default
- /mnt/user/appdata/featherdrop/config
- Value
- /mnt/user/appdata/featherdrop/config
Container path the SQLite database is written to. Must match the Config Directory mount above (/config), so leave it as /config.
- Target
- CONFIG_DIR
- Default
- /config
- Value
- /config
Public base URL for share links behind a reverse proxy, e.g. https://share.yourdomain.tld. Leave empty to use the browser address.
- Target
- BASE_URL
Expiry pre-selected in the UI when an uploader does not choose one. Pick from the dropdown: 1h, 6h, 1d, 7d, 30d, or never.
- Target
- DEFAULT_EXPIRY
- Default
- 7d|1h|6h|1d|30d|never
- Value
- 7d
Maximum upload size in bytes. 0 = unlimited (only limited by disk space). Example: 5368709120 = 5 GB.
- Target
- MAX_FILE_SIZE
- Default
- 0
- Value
- 0
Longest expiry an uploader may pick. 'never' (default) = no cap. Pick a shorter value to cap it; longer options (incl. 'never') then disappear from the UI and the server rejects them, and the Default Expiry is clamped to the cap.
- Target
- MAX_EXPIRY
- Default
- never|1h|6h|1d|7d|30d
- Value
- never
Optional. Maximum combined size of all stored shares, in bytes. 0 = unlimited. Uploads that would exceed the quota are rejected before any byte lands. Example: 53687091200 = 50 GB.
- Target
- STORAGE_QUOTA
- Default
- 0
- Value
- 0
Built-in per-IP rate limiting on uploads and download/password attempts (HTTP 429). Leave on unless you run your own limiter in a reverse proxy.
- Target
- RATE_LIMIT
- Default
- true|false
- Value
- true
Set true only when CastleParcel runs behind a reverse proxy: rate limits then use the client address from X-Forwarded-For. Leave it false on a directly reachable instance, because there any client could send its own X-Forwarded-For header and get around the rate limits.
- Target
- TRUST_PROXY
- Default
- false|true
- Value
- false
Optional. Replaces the 'CastleParcel' name in the header and the browser tab title with your own. Leave empty to keep 'CastleParcel'.
- Target
- APP_NAME
Optional. Your own logo (SVG/PNG) to replace the castle: either a public image URL, or a data: URI so you need no hosting and no file on disk. To make a data URI, run 'base64 -w0 logo.svg' and prefix the output with data:image/svg+xml;base64, (use image/png for a PNG). Leave empty to keep the castle.
- Target
- APP_LOGO
Optional. A 6-digit hex colour (e.g. #3daee9) for buttons, the upload ring and other accents. Leave empty to keep the gold (#d4af37).
- Target
- ACCENT_COLOR
Optional. If set, only people who know this password can upload / create shares; download links still work for everyone (instance-private uploads). Leave empty to allow open uploads. Use a value without leading or trailing spaces.
- Target
- UPLOAD_PASSWORD