# Running SourceHub on Unraid
Everything below was built and verified against a real Docker daemon: the image
builds, runs as `nobody:users`, launches Chromium unprivileged, crawls live sites,
and its data survives container recreation.
---
## What you get
One container running the web UI **and** the crawl scheduler. It stores everything --
database, downloaded images, browser login profile, and the editable YAML config --
under a single `/config` mount you point at appdata.
| | |
|---|---|
| Image size | **~4.2 GB** |
| Memory | ~400 MB idle, ~1.2 GB while a browser-rendered site is crawling |
| Appdata | ~16 MB after a small crawl; grows mostly with downloaded images |
| Ports | one, default `8000` |
| Volumes | one, `/config` -> `/mnt/user/appdata/sourcehub` |
| Database | SQLite, inside `/config` -- no second container needed |
The image is large because four of the working adapters (Banggood, eBay, AliExpress
detail pages, Chinavasion) render their listings in JavaScript and need a real
Chromium. It is built on Microsoft's official Playwright image rather than
`python:slim` -- Chromium pulls in around 90 system libraries, and hand-assembling
those on a slim base is a brittle apt incantation that breaks on Debian point
releases.
---
## Get the image
The image is published, so you do not need to build anything:
```bash
docker pull allornothing/shopping-hub:latest
```
It is around 4.2 GB, so give the pull a few minutes on a normal connection.
Building it yourself instead
Clone the repo onto a share and build on the Unraid box:
```bash
git clone https://github.com/aon082910/shopping-hub.git /mnt/user/isos/shopping-hub
```
```bash
cd /mnt/user/isos/shopping-hub && docker build -t allornothing/shopping-hub:latest .
```
Roughly 3-5 minutes, most of it pulling the Playwright base layer. Tagging it with
the same name means the template and compose file work unchanged.
---
## Add the container
**Easiest: Community Applications.** Open the **Apps** tab and search for
**shopping-hub**. Everything below is already filled in; just set the admin token.
The manual routes are below for anyone not using CA.
**Option A -- the template (recommended).** On the Unraid box:
```bash
wget -O /boot/config/plugins/dockerMan/templates-user/my-shopping-hub.xml \
https://raw.githubusercontent.com/aon082910/Unraid-CA/main/Shopping-Hub/shopping-hub.xml
```
Then
**Docker -> Add Container** and pick **shopping-hub** from the template dropdown. Every
setting below is already filled in and described in the form.
**Option B -- by hand.** Docker -> Add Container, then:
| Field | Value |
|---|---|
| Name | `SourceHub` |
| Repository | `allornothing/shopping-hub:latest` |
| Network Type | `Bridge` |
| Port | Container `8000` -> Host `8000` |
| Path | Container `/config` -> Host `/mnt/user/appdata/sourcehub` (Read/Write) |
| Extra Parameters | `--shm-size=1g` |
Add these variables:
| Variable | Value | Why |
|---|---|---|
| `SOURCEHUB_ADMIN_TOKEN` | *a long random string* | **Read the next section.** Without it the admin pages refuse to serve |
| `TZ` | `America/New_York` | The crawl schedule is interpreted in this zone |
| `PUID` | `99` | Unraid's `nobody` -- keeps appdata editable from the host |
| `PGID` | `100` | Unraid's `users` |
| `SOURCEHUB_MODE` | `serve` | Web UI and scheduler in one process |
Start it, then open `http://:8000`.
---
## The two settings that will bite you
### 1. `SOURCEHUB_ADMIN_TOKEN` -- admin is 403 without it
This is deliberate and it *will* look like a bug the first time. The admin pages
mutate your catalog (merging products, recording permanent match rejections), so they
refuse to serve when they are reachable from a non-loopback address and no token is
set. **In a container the client is never loopback** -- traffic arrives from the
Docker bridge -- so this fires on every containerised deployment, always.
Set the variable to any strong random string, restart, and sign in with it as the
**password** (any username works). Browsing, search, the API and crawling are
unaffected either way; only the pages that can modify data are gated.
Failing closed is the right default here: silently serving an open admin panel to
your whole LAN is the outcome nobody wants.
### 2. `--shm-size=1g` -- or browser-rendered sites go blank
Docker gives `/dev/shm` 64 MB by default. Chromium needs far more on heavy pages and
crashes when it runs out. The symptom is nasty because it is not an error: Banggood
and eBay simply return **zero listings**, which looks exactly like a broken selector.
Put `--shm-size=1g` in Extra Parameters and it goes away.
---
## First run
The container seeds `/config` on first start and says so:
```
[sourcehub] seeded config.yaml into /config (edit it there; the image copy is ignored)
[sourcehub] running as sourcehub (99:100), TZ=America/New_York
Database ready. 16 sites seeded.
[sourcehub] mode=serve (web UI + scheduler) on port 8000
[sourcehub] scheduled full_crawl 0 3 * * *
[sourcehub] scheduled refresh_prices 0 */6 * * *
```
(The `useradd warning: uid 99 outside of the UID_MIN 1000` line above it is normal --
Unraid's `nobody` is genuinely below the usual range.)
Your appdata now looks like this:
```
/mnt/user/appdata/sourcehub/
├── config.yaml which sites to crawl, keywords, schedule, matching weights
├── providers.yaml API presets for Taobao / Tmall / 1688
├── duty.yaml import duty rates (shipped disabled -- see below)
├── freight.yaml shipping weight and volumetric rates
├── .env optional; container variables override anything here
├── db/sourcehub.db the catalog
├── media/ downloaded product images
├── fixtures/ saved HTML snapshots used by the offline tests
└── browser_profile/ saved logins for the domestic-China sites
```
**Edit the YAML files here, not in the image.** They survive rebuilds and upgrades.
To see the UI populated without waiting for a crawl:
```bash
docker exec -u 99:100 SourceHub python -m sourcehub.cli demo-seed
```
Then a real crawl:
```bash
docker exec -u 99:100 SourceHub python -m sourcehub.cli crawl --sites dhgate,banggood --keyword "usb c hub" --pages 2
```
That exact command was run inside the container during testing: 81 listings, zero
errors, Chromium included.
---
## Running the CLI
Everything works through `docker exec`. Use `-u 99:100` so anything it writes stays
owned correctly:
```bash
docker exec -u 99:100 SourceHub python -m sourcehub.cli health
```
```bash
docker exec -u 99:100 SourceHub python -m sourcehub.cli selftest --site dhgate
```
```bash
docker exec -u 99:100 SourceHub python -m sourcehub.cli stats
```
`health` is the one worth scheduling. It compares each site's recent yield against
its own history, so it distinguishes *broken* (worked last week, zero today) from
*idle* (never worked), and exits non-zero when something needs attention -- which
makes it a clean **User Scripts** cron job.
---
## Scheduled crawling
`SOURCEHUB_MODE=serve` runs the scheduler inside the web container. Five jobs are
registered at boot; timings live in `config.yaml` under `schedule:` and are read in
your `TZ`:
```yaml
schedule:
full_crawl: "0 3 * * *" # 03:00 daily
refresh_prices: "0 */6 * * *" # every 6 hours
fx_rates: "30 2 * * *"
rematch: "0 5 * * 0" # Sundays
health_check: "0 7 * * *"
```
Edit, then restart the container.
A full sweep of the default keyword list across all enabled sites takes roughly 20-40
minutes, and it is deliberately slow -- the per-host rate limiter spaces requests
2.5-6 s apart. Do not tighten it. You will get your home IP blocked by the same sites
you are trying to buy from.
To split the UI and the crawler into separate containers (handy if you want to
restart the UI without interrupting a crawl), run a second container from the same
image with `SOURCEHUB_MODE=schedule` and no port mapping, and set the first to
`SOURCEHUB_MODE=web`. Point both at the same `/config`.
---
## Optional variables
None of these are required to boot; the app skips whatever capability is missing.
| Variable | Effect if unset |
|---|---|
| `TRANSLATE_PROVIDER` + `ANTHROPIC_API_KEY` | Chinese listings are not translated, so they cannot be title-matched and mostly stay on their own product pages. `claude`, `deepl`, `google_free` or `none` |
| `EBAY_CLIENT_ID` / `EBAY_CLIENT_SECRET` | eBay falls back to browser scraping -- works, just slower. Free keys at developer.ebay.com |
| `SOURCEHUB_PROXY` | Crawls come from your home IP. Fine for light use; a residential proxy is strongly recommended for sustained crawling. One URL, or several comma-separated for round-robin |
| `CN_PROVIDER_KEY` | Taobao / Tmall / 1688 stay unavailable without a browser login. See below |
| `OCTOPART_CLIENT_ID` / `SECRET` | Electronic-component part-number lookup is skipped |
Translations are cached by content hash, so a repeat crawl of the same listings costs
nothing.
---
## Taobao, Tmall and 1688 in a container
These three need either a one-time interactive browser login or an API key, and the
login is genuinely awkward in a container because it wants a visible browser window.
**Recommended:** get an API key and set `CN_PROVIDER_KEY` (presets are in
`providers.yaml`). No login, no browser, works unattended -- which is what you want on
a server anyway. Verify it before enabling the sites:
```bash
docker exec SourceHub python -m sourcehub.cli provider-probe --preset otapi --keyword "usb hub"
```
**Alternative:** do the browser login on a desktop with the project checked out
normally, then copy the resulting `data/browser_profile/` into
`/mnt/user/appdata/sourcehub/browser_profile/`. The cookies are portable. Expect to
redo it every few weeks.
Until you have done one or the other, leave those three `enabled: false` in
`config.yaml`, otherwise every crawl wastes minutes failing on them.
---
## Backup
Back up `/mnt/user/appdata/sourcehub` -- that is the entire application state. The CA
Appdata Backup plugin covers it with no special configuration. `media/` is the bulk of
it and is regenerable by re-crawling, so exclude it if you want a small backup.
---
## Updating
```bash
docker pull allornothing/shopping-hub:latest
```
Then **Docker -> SourceHub -> Force Update**, or stop and start it.
Your YAML files in `/config` are never overwritten. A pristine copy of the defaults
lives at `/defaults` inside the image, so after an upgrade you can see what changed:
```bash
docker exec SourceHub diff /defaults/config.yaml /config/config.yaml
```
Settings added by an upgrade show as missing from your copy. Add them by hand.
---
## Troubleshooting
**Admin returns 403.** Set `SOURCEHUB_ADMIN_TOKEN`. Expected in a container -- see
above. With the token set you get a normal HTTP Basic prompt: any username, the token
as the password.
**Banggood or eBay return zero listings.** Almost always a missing `--shm-size=1g` in
Extra Parameters.
**Permission denied writing to appdata.** Check `PUID=99` / `PGID=100` and that the
share is not read-only. The entrypoint chowns `/config` at start and warns in the log
if it cannot.
**A site that used to work now finds nothing.** Run `health` (above) to confirm, then
`selftest --site ` to see the live failure. These sites change their markup
without notice; that is why both commands exist.
**Everything fails with CERTIFICATE_VERIFY_FAILED.** Something on your network is
intercepting TLS -- antivirus HTTPS scanning is the usual culprit:
```bash
docker exec SourceHub python -m sourcehub.cli trust-setup
```
It probes a live certificate and reports which authority actually signed it.
**Container restarts in a loop.** `docker logs SourceHub`. The entrypoint prints each
step as it happens, so the failing stage is normally the last line.
**Is it alive?** `http://:8000/healthz` returns 200 when it is. That is also
the container's own HEALTHCHECK, so Unraid's Docker tab shows it as healthy.
---
## Notes on what it will not do
Duty ships **disabled with an empty rate table**, because rates depend on HTS
classification and change. Enable it in `duty.yaml` only if you have real numbers for
the goods you actually import -- a plausible-looking wrong duty rate is worse than
none.
Three adapters (LCSC, Tomtop, Global Sources) ship **disabled** with the reason
recorded in `config.yaml`. Global Sources in particular was ingesting navigation links
as products, which is worse than returning nothing.
And the tool will not make importing cheaper than it is. The eBay baseline exists
precisely because buying domestically sometimes wins, and the break-even page will
tell you when.