Files
gitea d0c7ab61a7
CI / compile (pull_request) Successful in 4s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 24s
build / build (push) Successful in 7s
CI / compile (push) Successful in 5s
CI / unit (push) Successful in 22s
CI / integration (push) Successful in 24s
docs: from-zero node runbook for the share service
FILE_SHARING.md explains how the feature works and assumes you already know
the node story; there was nothing describing what a fresh box has to provide.
SHARE_NODE_SETUP.md fills that: the four host directories, the uid-33
readability requirement on the media library, deploying the musician+share
stack (Portainer or CLI), forwarding /share from the reverse proxy, and the
musician-side CONJURER_SHARE_* wiring - each with its verification command.

Calls out the three couplings that actually break it in practice: the media
must be mounted at the SAME container path on both containers (index paths and
symlinks are absolute), the link/index dirs must be shared, and
CONJURER_SHARE_BASE_URL must match the real public URL - it is not set in the
bundled stack file and silently falls back to czernobog.pl.

Also records what the node does NOT need: no host Apache, no cron, no Python,
no hand-placed scripts - Apache, the vhost, scan_shares.py and
revoke_shares.py are all baked into the image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 17:33:18 +02:00

5.8 KiB

Share node setup — from zero

Everything the share service needs is inside the image: Apache, the vhost, the index scanner and the link revoker. The node needs no Apache, no cron, no Python and no copies of the old hand-placed scripts.

What the node actually provides is four directories, the media library, and a way in from the internet.

Feature docs (how links work, security model, TTL semantics) live in FILE_SHARING.md. This file is only "how to stand it up on a fresh box".

What runs where

Piece Where it lives Notes
Apache + vhost in the image rendered from share-vhost.conf.tpl at start
scan_shares.py (index) in the image sleep loop, not cron
revoke_shares.py (expiry) in the image sleep loop, not cron
the symlinks host /srv/share/links created by the musician, served here
the index host /srv/share/db written here, read by the musician
TLS / public name your reverse proxy container speaks plain HTTP

The single most important fact: the musician creates the links, this service serves them. They must see the same directories, at the same paths.

1. Directories

sudo mkdir -p /srv/share/{media,links,db,logs}
Path Contents Mounted as
/srv/share/media the media library /mnt/shares (read-only)
/srv/share/links published symlinks /var/www/html/share
/srv/share/db share_scan.json /srv/share/db
/srv/share/logs Apache logs incl. share_access.log /var/log/apache2

2. Media library and permissions

Put the library at /srv/share/media (bind mount, NFS mount, whatever — it is only ever read). Apache serves as www-data, uid 33 inside the container, so that uid must be able to traverse and read it:

sudo chmod -R o+rX /srv/share/media          # simplest; or use ACLs/group instead
sudo -u '#33' test -r /srv/share/media/<some-file> && echo "readable by www-data"

A library that root can read but uid 33 cannot is the classic "every link 404s / 403s" cause.

3. Deploy the stack

The musician and the share service share a filesystem, so the supported layout is both on the same node, from one stack:

  • Portainer → Stacks → Add stack → paste docker/compose.musician-share.stack.yaml
  • or CLI: docker compose -f docker/compose.musician-share.stack.yaml up -d

That stack already wires the four mounts on both containers. Set CONJURER_SHARE_SERVER_NAME to your public hostname.

Share-only node (musician elsewhere): use docker/compose.share.yaml and put /srv/share/links + /srv/share/db on storage both hosts mount — otherwise the musician cheerfully creates links this container cannot see.

4. Way in from the internet

The container listens on 8081 → 80, plain HTTP by design; TLS stays on the reverse proxy you already run. Forward the /share/ prefix unchanged — the links are https://<host>/share/<token>:

nginx:

location /share/ {
    proxy_pass http://<share-node-ip>:8081/share/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Apache (as reverse proxy):

ProxyPass        /share/ http://<share-node-ip>:8081/share/
ProxyPassReverse /share/ http://<share-node-ip>:8081/share/

Then: DNS for the public name points at the proxy, and the node's firewall lets the proxy reach 8081 (nothing else needs to).

5. Tell the musician where to publish

The musician builds the URLs it posts to Discord. In its env:

CONJURER_SHARE_DIR=/var/www/html/share
CONJURER_SHARE_DB=/srv/share/db/share_scan.json
CONJURER_SHARE_BASE_URL=https://czernobog.pl/share

CONJURER_SHARE_BASE_URL is not set in the bundled stack file — it falls back to https://czernobog.pl/share. If your public name differs, set it explicitly or every posted link points at the wrong host.

The three couplings that break it

  1. Same container path for the media. The index records absolute paths and the symlinks are absolute. Both containers must mount the library at /mnt/shares. Mount it elsewhere on one side and every link dangles.
  2. Same link dir and index dir for musician and share (same host, or shared storage).
  3. CONJURER_SHARE_BASE_URL must equal your real public /share URL.

Verification

# index built (should be non-trivial JSON)
sudo head -c 200 /srv/share/db/share_scan.json; echo

# the two jobs and Apache are alive
docker logs conjurer-share | tail -20     # "[share] serving ... scan every ...s"

# directory listing MUST fail (403) - it would leak every live token
curl -sI http://<share-node-ip>:8081/share/ | head -1

# revoker state files must NOT be served
curl -sI http://<share-node-ip>:8081/share/.downloads.json | head -1

# a real link: publish one from Discord, then
curl -sI https://<public-host>/share/<token> | head -1   # 200

After the first download of a link, revoke_shares.py removes it once CONJURER_SHARE_TTL_SECONDS (default 1h) has passed. A link nobody downloads is never revoked by that job.

Troubleshooting

Symptom Cause Fix
every link 404 media mounted at a different container path than /mnt/shares, or the symlink target is gone align the mounts on both containers
every link 403 media not readable by uid 33 step 2
links created but not served musician and share not sharing /srv/share/links step 3
/get_share_list empty index missing/not shared check /srv/share/db/share_scan.json and the musician's CONJURER_SHARE_DB
links never expire revoker cannot see the access log, or nobody downloaded them check /srv/share/logs/share_access.log exists and grows
wrong host in posted links CONJURER_SHARE_BASE_URL step 5