A clean, login-free, self-hosted file sharer. Drop a file, set an expiry, share a link.
415
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
#fragment or comes from the share's password, so the server and anyone with its disk only ever see ciphertext.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:
castleparcel-client.json, with the SHA-256 of every file of the client and one root hash over all of them./.well-known/castleparcel-client.json and prints the root hash in its log at startup.index.html pins 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.blob: URL made from the script's own code, so there is no separate worker file to check.sw-download.js cannot 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:
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.
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. |
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.
junkerderprovinz/castleparcel:latest and apply. Leave the paths as they are.junkerderprovinz/castleparcel:latest with the same /data and /config folders.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 ./appdata folders as the old one.The featherdrop images stay on Docker Hub and GHCR, but they get no more updates.
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.
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.
Content type
Image
Digest
sha256:0d0453678…
Size
8.4 MB
Last updated
about 7 hours ago
docker pull junkerderprovinz/castleparcel