CastleParcel

CastleParcel

Docker app from junkerderprovinz's Repository

Overview

CastleParcel is a self-hosted file sharer for big files, like the transfer site everyone already knows, minus the accounts, the size paywalls and someone else's cloud, and sealed so that not even your own server can read what you send. Open the page, drop a file, set an expiry (plus an optional password or download limit), and share a short link or QR code. Files are end-to-end encrypted in your browser before upload, so the server only stores ciphertext and never sees your files, their names or the key. Uploads are resumable (tus), files sit on a local volume and the metadata lives in a single SQLite file, so there is no separate database, no account system and no tracking. The UI comes in 42 languages with light and dark mode, and you can brand it with APP_NAME, APP_LOGO and ACCENT_COLOR. Put it behind a reverse proxy and set BASE_URL so share links use your public domain. CastleParcel used to be called featherdrop. The appdata folder keeps that name, so an existing install keeps its shares and every link still opens.

CastleParcel

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.


Table of Contents

  1. Download
  2. Support
  3. What it looks like
  4. What it does
  5. Threat model
  6. Getting started
  7. How AI is used here

1. Download

Server

Install from Unraid's Community Applications   Install from the MOS Hub   Install from the ZimaOS and CasaOS store   Install from the umbrelOS community app store

Download the docker-compose file   Download the source archive


2. Support

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.

Buy me a coffee   PayPal   Donate with crypto


3. What it looks like

The files and links in these pictures are made up.

CastleParcel in a browser window: a photo is selected and the options for expiry, download limit, password and photo metadata are shown
Pick the files, then decide how long they live and how often they can be downloaded

CastleParcel showing the finished share link with a QR code and the management link
The link is ready: copy it or save the QR code

The share page a recipient opens, with the files, their sizes, the expiry and a download button
What the recipient sees, decrypted in their browser


4. What it does

  • Zero-knowledge. Files are encrypted in your browser before they upload (libsodium XChaCha20-Poly1305), names included. The key lives in the link's #fragment or 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.


5. Threat model

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.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.
  • 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.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:

  • 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.


6. Getting started

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:latest and apply. Leave the paths as they are.
  • docker run: start junkerderprovinz/castleparcel:latest with the same /data and /config folders.
  • 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 ./appdata folders 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.

7. How AI is used here

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.

Download Statistics

160
Total Downloads

Related apps

Details

Repository
junkerderprovinz/castleparcel:latest
Last Updated2026-11-10
First Seen2026-11-11

Runtime arguments

Web UI
http://[IP]:[PORT:3000]/
Network
bridge
Shell
bash
Privileged
false
Extra Params
--restart unless-stopped

Template configuration

WebUI PortPorttcp

Port the CastleParcel web interface listens on.

Target
3000
Default
3000
Value
3000
Data Directory (uploads)Pathrw

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
Config Directory (database)Pathrw

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
Config Dir (container path)Variable

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 URLVariable

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
Default ExpiryVariable

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
Max File Size (bytes)Variable

Maximum upload size in bytes. 0 = unlimited (only limited by disk space). Example: 5368709120 = 5 GB.

Target
MAX_FILE_SIZE
Default
0
Value
0
Max ExpiryVariable

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
Storage Quota (bytes)Variable

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
Rate LimitVariable

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
Trust ProxyVariable

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
Custom App NameVariable

Optional. Replaces the 'CastleParcel' name in the header and the browser tab title with your own. Leave empty to keep 'CastleParcel'.

Target
APP_NAME
Custom Logo URLVariable

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
Accent ColorVariable

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
Upload PasswordVariable

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