diff --git a/README.md b/README.md index 72d6ede..ce4502b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ handled separately — see [Exposing services](#exposing-services). | [audiobookshelf](audiobookshelf/) | 13378 | Audiobooks and podcasts, with per-user progress sync | | [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port | | [codeserver](codeserver/) | 8443 | VS Code in a browser. Effectively a remote shell — read its README before exposing | +| [copyparty](copyparty/) | 3923 | File server built for big/resumable transfers, WebDAV, media index | | [filebrowser](filebrowser/) | 3672 | Web file manager for one directory tree (FileBrowser Quantum fork) | | [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [openwebui](openwebui/) | 3033 | LLM chat UI. One `OWUI_TAG` switches between CPU, CUDA and bundled-Ollama | diff --git a/copyparty/.env.example b/copyparty/.env.example new file mode 100644 index 0000000..21d6138 --- /dev/null +++ b/copyparty/.env.example @@ -0,0 +1,31 @@ +# Copy to .env and adjust. .env is git-ignored; this file is not. + +# ─── Required ─────────────────────────────────────────────────────── +# Password for the account named in init/copyparty.conf. That config grants +# access to this account ONLY -- there is no anonymous access. +# +# openssl rand -base64 18 +# +CP_PASSWORD= + +# The directory to share. Bound to /w inside the container, which is what +# init/copyparty.conf serves at the webroot. +# Add `:ro` to the volume line in compose.yaml for a download-only server. +SHARE_PATH=/srv/downloads + +# ─── Optional ─────────────────────────────────────────────────────── +# Username. Change it here AND in the `accs:` block of init/copyparty.conf -- +# the two must match or the account has no permissions. +#CP_USER=admin + +# Index, thumbnails and copyparty's own config dir. Keeps state out of the +# shared folder. Back this up. +#DATA_PATH=./data + +# The image runs as root unless told otherwise and reads no PUID/PGID; these +# feed Docker's `user:` directive. Must be able to read/write SHARE_PATH. +#PUID=1000 +#PGID=1000 +#TZ=Europe/Paris + +#PORT=3923 diff --git a/copyparty/README.md b/copyparty/README.md new file mode 100644 index 0000000..114b67d --- /dev/null +++ b/copyparty/README.md @@ -0,0 +1,94 @@ +# copyparty + +File server with an unusual amount packed in: resumable uploads, dedup, a media +index with thumbnails, share links, and WebDAV/FTP/SFTP/TFTP on the side. One +Python process, no database server. + +Overlaps with [filebrowser](../filebrowser/) — that one is a tidy file manager, +this one is a transfer workhorse. Uploading 40 GB over a flaky connection is +where the difference shows. + +## Quick start + +```bash +cp .env.example .env +echo "CP_PASSWORD=$(openssl rand -base64 18)" >> .env +$EDITOR .env # set SHARE_PATH +docker compose up -d +``` + +Open `http://:3923` and log in as `admin` with that password. + +## Ports + +| Port | Proto | Purpose | +|------|-------|---------| +| 3923 | http | Web UI, WebDAV, and the API | + +## Why it looks like this + +**No anonymous access.** Upstream's example config grants `rw: *` — read *and +write* to anybody who can reach the port. That's a sensible LAN dropbox and a +poor default for anything else, so this config gives access to one named +account and nothing to the public. Verified: anonymous gets `403`, the account +gets `200`. + +**The password is not in the config file.** copyparty's `[accounts]` section +takes `user: password` in plaintext, which is fine on your own disk but not in +a git repo. Accounts are passed as a command-line argument instead, so +`CP_PASSWORD` stays in `.env`: + +```yaml +command: ["-a", "${CP_USER:-admin}:${CP_PASSWORD:?...}"] +``` + +The username there must match the `accs:` block in +[`init/copyparty.conf`](init/copyparty.conf), or the account exists with no +permissions and every page looks empty. + +**`XDG_CONFIG_HOME=/data`.** `/cfg` is mounted read-only, so without this +copyparty can't write its config directory and falls back to `/tmp` — logging a +warning most people scroll past. The consequence is that **the filekeys behind +every share link regenerate on each restart**, quietly breaking links you've +handed out. + +**`hist: /data/hist`.** By default the index and thumbnail cache go into a +`.hist` directory *inside the folder you're sharing*. Redirecting it keeps your +media directory as you left it. + +**The `ac` image.** Variants differ by what's bundled — `min` is the bare +server, `ac` adds ffmpeg for thumbnails and media tags, others add more. `ac` is +what the config's `e2ts` needs. + +## Gotchas + +- **Inline comments need two spaces before the `#`.** With one, the `#` and + everything after becomes part of the value. copyparty warns at startup rather + than failing, so it's easy to miss. +- **Share links bypass authentication by design.** That's the point of them, + but a leaked link is public access to that file. +- **`e2dsa` indexes on first start.** For a large share, expect the first boot + to churn; uploads are blocked while it runs and it says so in the log. +- **Back up `DATA_PATH`.** It holds the index and the filekeys that make share + links stable. +- **Version-check is opt-in.** Uncomment `vc-url` and `vc-exit` in the config + and copyparty refuses to start on a version with a known advisory — worth it + if you expose this. + +## Exposing it + +- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — needed; the built-in + TLS uses a self-signed certificate. +- [Single sign-on with Authentik](../docs/authentik-sso.md) — copyparty has its + own accounts, so forward auth means two logins. Worth it anyway if this faces + the internet. +- [Cloudflare Tunnel](../cloudflare-tunnel/) — **think first.** Large uploads + are the reason to run copyparty, and the tunnel's ~100 MB body cap is exactly + what breaks. + +## Links + +- Upstream: +- The compose example this started from: + +- Image: diff --git a/copyparty/compose.yaml b/copyparty/compose.yaml new file mode 100644 index 0000000..e92a01d --- /dev/null +++ b/copyparty/compose.yaml @@ -0,0 +1,48 @@ +services: + copyparty: + # Variants differ by what's bundled: `min` is the bare server, `ac` adds + # ffmpeg for thumbnails and media tags, `iv`/`dj` add more. `ac` is the + # sensible default and what the config's e2ts flag needs. + image: copyparty/ac:1.20.21 + container_name: copyparty + restart: unless-stopped + + # Runs as root unless told otherwise. It reads no PUID/PGID. + user: "${PUID:-1000}:${PGID:-1000}" + + # The thumbnailer gets ~10s to finish after the stop signal. + stop_grace_period: 15s + + environment: + - TZ=${TZ:-Etc/UTC} + # Keeps log lines from being buffered. + - PYTHONUNBUFFERED=1 + # Without this, copyparty cannot write its config dir (/cfg is read-only) + # and falls back to /tmp -- which silently regenerates the filekeys behind + # every share link on each restart. + - XDG_CONFIG_HOME=/data + # Replace NOPE with 2 to enable mimalloc: faster, roughly double the RAM. + - LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE + + # Accounts live here, not in the committed config file. The name must match + # the one in init/copyparty.conf's `accs:` block. + command: ["-a", "${CP_USER:-admin}:${CP_PASSWORD:?set a password - the config grants access to this account only}"] + + volumes: + - ./init:/cfg:ro + # Index, thumbnails and copyparty's own config dir. Back this up. + - ${DATA_PATH:-./data}:/data + # What you're sharing. Add `:ro` for a download-only server. + - "${SHARE_PATH:?set SHARE_PATH to the directory you want to share}:/w" + + ports: + - "${PORT:-3923}:3923" + + healthcheck: + # The "/._" suffix matches copyparty's default log filter, so the probe + # doesn't fill the log. + test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset=/._"] + interval: 1m + timeout: 2s + retries: 5 + start_period: 15s diff --git a/copyparty/init/copyparty.conf b/copyparty/init/copyparty.conf new file mode 100644 index 0000000..8072a9a --- /dev/null +++ b/copyparty/init/copyparty.conf @@ -0,0 +1,40 @@ +# copyparty configuration. Everything in /cfg is loaded by the image's own +# initcfg, which also sets `chdir: /w`. +# +# Not YAML despite appearances -- indentation matters, `key: value` for options +# that take one, bare words for flags. +# +# Inline comments need AT LEAST TWO SPACES before the `#`. With one space the +# `#` and everything after it becomes part of the value, and copyparty warns +# about it at startup rather than failing. +# +# No [accounts] section here on purpose: accounts are passed on the command +# line from .env, so no password is ever committed. See compose.yaml. + +[global] + e2dsa # index files and scan the filesystem + e2ts # read media tags; needs the "ac" image, which has ffmpeg + + # Keep the index out of the shared folder. Without this, copyparty creates + # a .hist directory inside whatever you are sharing. + hist: /data/hist + + # ipa: lan # reject connections from outside the LAN + # df: 16 # refuse uploads when under 16 GB free + # grid # thumbnail grid by default + # theme: 2 # monokai + + # Warn -- and with vc-exit, refuse to start -- if the running version has a + # known advisory. Off by default upstream; worth it for anything exposed. + # vc-url: https://api.copyparty.eu/advisories + # vc-exit + +[/] # volume at the webroot + /w # serving /w, which compose binds to your SHARE_PATH + accs: + # Upstream's example uses `rw: *`, i.e. read-write for anonymous visitors. + # That is a reasonable LAN dropbox and a bad default for anything else, so + # only the account created in compose.yaml gets access here. + rwmda: admin # read-write-move-delete-admin + flags: + e2ds