# Dropnook — for developers and power users The [README](../README.md) covers installing and using Drop with Unraid's Compose Manager Plus. This page has the rest. ## All settings Set as environment variables in `compose.yaml`. Your own values come from the `.env` next to it (`${PIN:-}`, template `compose-projects-drop/.env.example`), so `compose.yaml` itself holds none and can be replaced as a whole; Compose Manager Plus passes its *.env* tab with `--env-file`, plain `docker compose` reads `.env` by itself. Besides the variables below that it passes on (`CERTS_DIR`, `TLS_CERT`, `TLS_KEY`, `SHARE_SUBDOMAIN`, `PIN`, `USER_*`, `TZ`, `WEBUI`, `HOSTS`), the `.env` holds what only Compose uses: `DROP_IP`, `SHARE_IP` (required), `DATA_DIR` and `APPDATA_DIR`; `WEBUI` is also the label for Unraid's *WebUI*. Anything else from the table goes under `environment` of `drop`. | Variable | Default | Meaning | |---|---|---| | `MODE` | `lan` | `lan` = Drop itself, `public` = share links only (`drop-share`) | | `APP_NAME` | `{server} Drop` | Page title; `{server}` is the Unraid server name | | `APP_NOTICE` | empty | A short line shown in the header; empty hides it | | `PIN` | empty | PIN or password every new browser is asked for once (signed in for 90 days); changing it signs everyone out. With users: the PIN of every user without one of their own. `lan` only | | `USER_TEAL`, `USER_GOLD`, `USER_BLUE`, `USER_VIOLET`, `USER_CORAL` | empty | Up to five users, one per colour: `name:PIN`, or `name` alone — then `PIN` is theirs, or, without `PIN`, the name alone signs in. Each gets an area of their own in `USERS_DIR//` next to the shared one. Names: 1–40 characters, no `/` or `\`, not starting with a dot, each once — otherwise Drop does not start. `lan` only | | `TEXT_FIELDS` | `3` | Fields after the first start and after "Clear text fields" | | `TEXT_FIELDS_MAX` | `12` | Maximum number of fields | | `CHUNK_MB` | `64` | Upload chunk size | | `SHARING` | `on` | `off` hides sharing to the internet | | `SHARE_MAX_DAYS` | `30` | Longest lifetime of a share link | | `SHARE_SUBDOMAIN` | `drop-share` | First part of the share links' name, derived from the name Drop is opened under | | `SHARE_BASE_URL` | empty | Fixed link address instead, e.g. `https://drop-share.yourdomain.com` | | `WEBUI` | empty | The address Drop is opened under; its name is one Drop answers to. Drop answers only to IP addresses, names without a domain or with a local one (`.local`, `.lan`, `.home`, `.home.arpa`, `.internal`, `.fritz.box`, `.ts.net` and a few more), the names of its certificates, and these — anything else gets 421 "Unknown name" (against DNS rebinding). `lan` only | | `HOSTS` | empty | More such names, comma-separated; `*.example.org` for every name under example.org. `lan` only | | `TLS_CERT` | empty | `auto`, a domain, or the path of the certificate chain — see the README, "HTTPS in your network" | | `TLS_KEY` | empty | Path of the private key, only with a path in `TLS_CERT` | | `CERTS_DIR` | `/certs` | Where `auto` and a domain look for certificates (`live/*/` and `*/` with `fullchain.pem` + `privkey.pem`) | | `TLS_AUTO_NAME` | `drop` | Host name the certificate search looks for: `.` or `*.`. One certificate per domain; with several, the one matching the requested name is served (SNI), the longest-lived answers requests by IP | | `HTTPS_HOST` | empty | Target name for the HTTP → HTTPS redirect when a client sends no Host header | | `HTTP_REDIRECT_PORT` | `80` | Port of that redirect | | `PORT` | `80` / `443` / `8080` | Listening port: `lan` without / with TLS, `public` (compose sets 80) | | `PUID`, `PGID` | `99`, `100` | Owner of new files | | `SERVER_NAME` | empty | Server name, if it is not read from Unraid | | `UNRAID_IDENT` | `/unraid/ident.cfg` | Where the Unraid server name is read from | | `FILES_DIR`, `TEXTS_DIR`, `SHARES_DIR` | `/data/files`, `/data/texts`, `/data/shares` | Data folders inside the container; files and texts of the shared area | | `USERS_DIR` | `/data/users` | The users' areas, `/files` and `/texts` each — in the same mount as `shares/`, for the hard links | | `THUMBS_DIR` | `/data/.thumbs` | Cache of the picture previews, next to `files/` | | `CUSTOM_DIR` | `/config` | Folder with `lang/` and `help/` files of your own (the appdata folder) | | `TZ` | UTC | Only the time stamps in the container log | ## Without Unraid Unraid with Compose Manager Plus is the recommended way, and what the README describes. But `compose-projects-drop/compose.yaml` is a plain Compose file and runs on any Linux host with Docker. Put it in a folder of its own, next to it a `.env` from `.env.example` with addresses and paths of that host, and change three things: * **Network.** `br0` is Unraid's network for containers with their own LAN address. Elsewhere, create one like it once (ipvlan on your network card, with your LAN's subnet and gateway) and keep the name, or rename it in the file: ```sh docker network create -d ipvlan --subnet 192.168.1.0/24 --gateway 192.168.1.1 \ -o parent=eth0 -o ipvlan_mode=l2 br0 ``` The host itself cannot reach these container addresses — a reverse proxy on the same host needs its own address in that network, or reaches `drop-share` from another machine. * **Server name.** Remove the `/boot/config/ident.cfg` line and set `SERVER_NAME: ` (or `APP_NAME`) under `environment` of `drop`. * **Data folder.** `DATA_DIR` must exist before the first start (Drop does not create it); `drop` creates `files/`, `texts/` and `shares/` in it, owned by `99:100` (`PUID`/`PGID` to change that). Start with `docker compose up -d` next to the file; `docker compose pull && docker compose up -d` updates. The `net.unraid.*` labels do nothing outside Unraid and can stay. ## How the pieces work * **One image, two modes.** `MODE=lan` serves Drop itself (FastAPI, live updates over Server-Sent Events, chunked resumable uploads). `MODE=public` serves `/`, `//download` and `/robots.txt`, nothing else, and aborts if it can see `/data/files` or `/data/texts`. * **Picture previews** (`/api/thumb/`) are made by Pillow on first request — PNG, JPEG, GIF, WebP, BMP, AVIF only (`Image.open(formats=…)`, so no other Pillow plugin reads the file); at most 80 MB and 60 megapixels, two at a time — and kept as small WebP files in `THUMBS_DIR`, named after the file's name, size and mtime. Previews of deleted files are removed. SVG is shown by the browser and never decoded on the server; the public container never loads Pillow. * **Shares** live in `shares/`: one record `.json` each, files as hard links in `shares/files/`, view counters written by `drop-share` to `shares/counters/`, the cookie secret for password-protected links in `shares/.key`. `drop-share` uses a record only if its `id` is the hash it was looked up by and its fields have the shape `drop` writes. * **Password attempts** are counted before the (deliberately slow, PBKDF2, 600 000 rounds; links from before 2.0.1 keep their 200 000) check, so parallel guesses cannot exceed 10 per link and 15 minutes. * **Who may talk to `drop`.** Requests with a proxy header (`X-Forwarded-*`, `Forwarded`, `Via`, …), from a public address, or for `drop-share.` get 404 — also with `SHARING=off`. Names other than its own get 421 (see `WEBUI` above). Every answer carries a CSP (`default-src 'self'`, no framing), `nosniff` and `Cross-Origin-Resource-Policy: same-origin`; downloads also `sandbox`. JSON is taken only as `application/json`, so a form on another site cannot send it without the browser asking first. * **Limits.** JSON requests up to 16 MB, a text field up to 2 million characters, an upload only if it fits the free space. An upload without progress for 10 minutes no longer holds back a certificate renewal; after 7 days it is given up. A resumed upload has to match name, size and — where the browser knows it — the file's last-modified time. * **The share sheet** (`POST /share-target`, from the manifest's `share_target`) is the one write a plain HTML form on another site could send. It is taken only with `Sec-Fetch-Site: none` (the share sheet) or `same-origin`. Files are copied under a hidden `.shared-*` name and renamed when complete; large parts wait in `/data/.tmp` (`tempfile.tempdir`), never in the container. The app icons come from `static/icon.svg` (`tools/screenshots/icons.mjs`). * **The optional PIN** (`PIN`): without the cookie, `drop` answers only the sign-in page (`/`, `POST /login`) and `/api/help` (Docker's health check); everything else is 401, and the page reloads on it. The cookie holds the time of the sign-in, signed with HMAC over that time and a hash of the PIN, keyed by `/data/.access-key` — which `drop-share` never sees. HttpOnly, SameSite=Strict, Secure with HTTPS, 90 days. Wrong PINs, counted before the check, per 15 minutes: 10 per address and PIN, 30 per PIN, 50 in all — a sign-in with one's own PIN does not reset the count for another one. * **Users** (`USER_`): the sign-in page lists them by name and colour; a user without PIN signs in with a click. The cookie is `