# 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](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 ```bash 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: ```bash sudo chmod -R o+rX /srv/share/media # simplest; or use ACLs/group instead sudo -u '#33' test -r /srv/share/media/ && 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:///share/`: nginx: ```nginx location /share/ { proxy_pass http://:8081/share/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ``` Apache (as reverse proxy): ```apache ProxyPass /share/ http://:8081/share/ ProxyPassReverse /share/ http://: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: ```ini 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 ```bash # 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://:8081/share/ | head -1 # revoker state files must NOT be served curl -sI http://:8081/share/.downloads.json | head -1 # a real link: publish one from Discord, then curl -sI https:///share/ | 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 |