Runs the web app as one Docker container on a home NAS, reachable from any browser on your LAN (and optionally over Tailscale). Written for a small x86_64 NAS running Docker; the steps are the same on any consumer NAS or Linux box, and only the notes at the end are vendor-specific.
Placeholders used throughout:
| Placeholder | Meaning | Example |
|---|---|---|
<your-nas> |
the NAS hostname or LAN IP | nas.local, 192.0.2.10 |
<share-path> |
the shared folder you deploy into | /volume1/apps |
<port> |
the LAN port the app listens on | 8321 |
- x86_64 CPU — the tested platform. Current pymeshlab and cadquery-ocp releases do publish Linux aarch64 wheels, so an ARM NAS is no longer ruled out by packaging alone — but nothing in this stack has been verified on ARM Linux, and the Poisson step has already proven platform-sensitive (it crashes on macOS arm64). Treat ARM as untested, not supported.
- RAM — 8 GB minimum, 16 GB comfortable. A 2-million-triangle scan peaks at several GB during Poisson repair.
On a vendor NAS (menu names vary by firmware; these are typical):
- App Center → install Docker (packaged as "DockerEngine" on some
firmwares). Confirm over SSH:
docker compose version. - App Center → install Git (optional — the deploy script falls back to a
throw-away
alpine/gitcontainer if there is nogitbinary). - Control Panel → Shared Folders → create a plain share for apps
(no encryption needed; the app stores only uploaded meshes and generated
STEP files, purged after 24 h). Its path is your
<share-path>. - Enable SSH (Control Panel → Terminal & SNMP) and log in as an admin user.
On any other NAS: install Docker (with Compose v2), create a folder, enable SSH.
cd <share-path>
git clone https://github.com/Crypto69/stlToSolid.git
cd stlToSolid
chmod -R a+rX . # some NAS shares strip file modes on checkout; harmless elsewhere
mkdir -p data # job storage, bind-mounted to /data inside the containerdocker compose buildBuilds natively on the NAS (a few minutes on a NAS CPU; it downloads the
~2 GB CAD stack the first time). Do not copy an image built on an
Apple-silicon Mac unless you built it with --platform linux/amd64.
If the build fails with a download timeout in pip or apt-get, just run it
again — it is a mirror hiccup, not a code problem (pip is set to 300 s / 10
retries; pymeshlab alone is a 106 MB wheel).
docker compose up -d
docker compose ps # state: running
curl -s http://127.0.0.1:<port>/ | head # serves the UI's index.htmlOpen http://<your-nas>:<port> in a browser, drop an STL, press Convert.
The report card with green PASS gates is the acceptance test. The badge
top-right shows version · commit · build time (also at /api/version).
cd <share-path>/stlToSolid
./deploy.sh # pulls main, fixes share file modes, builds, restarts
./deploy.sh --no-pull # rebuild what is checked out, without pullingA restart kills in-flight conversions; re-run them from the browser.
If the project's git history was ever rewritten upstream (force-push), a plain pull refuses. Reset once, then deploy as usual:
git fetch origin && git reset --hard origin/main && ./deploy.shAll settings live in docker-compose.yml; edit, then docker compose up -d.
| Setting | Default | Meaning |
|---|---|---|
ports |
8321:8000 |
LAN port (change the left side) |
STLTOSOLID_JOB_TTL |
86400 |
seconds before job directories are deleted |
STLTOSOLID_CONCURRENCY |
1 |
parallel conversions; keep 1 unless RAM is plentiful |
STLTOSOLID_MAX_UPLOAD |
200 MB | maximum upload size |
STLTOSOLID_WORKERS |
2 |
shells (bodies and cavities) converted at once inside one job; each worker holds a few hundred MB |
STLTOSOLID_SHELL_TIMEOUT |
900 |
seconds a shell may run in a worker before it is built faceted instead; 0 means no limit |
STLTOSOLID_AXIS_BUDGET |
120 |
seconds one shell's extrusion-axis search may take before the best candidate so far is used; 0 means no limit |
mem_limit |
12g |
container memory cap; lower it if other services suffer |
data/ is disposable: rm -rf data/* with the container stopped is always
safe. There is no database and nothing to back up.
If the NAS is on your tailnet, Tailscale can front the app with HTTPS without opening anything to the internet:
tailscale serve --bg --https=8443 http://127.0.0.1:<port>
tailscale serve statusThe app is then at https://<your-nas>.<your-tailnet>.ts.net:8443 — run
tailscale status to see your own tailnet name. Use a port nothing else is
serving (tailscale serve status lists the entries). The app has no login:
anyone on your LAN or tailnet can use it, which is the intended scope.
tests/ and samples/ are not baked into the image. Mount them from the
checkout (copy your sample meshes into samples/ first):
docker compose run --rm \
-v "$PWD/tests:/app/tests:ro" -v "$PWD/samples:/app/samples:ro" \
stltosolid sh -c 'pip install -q pytest httpx && python -m pytest -q -m "not slow"'
# use `-m slow` instead for the scan end-to-ends (takes minutes)This is also where the scan path gets proven: Poisson repair does not work on macOS arm64, so a large scan converting to a valid solid here is the platform acceptance test.
restart: unless-stopped brings the container back by itself. Check
docker compose ps; if you use the Tailscale front, run
tailscale serve status and repeat the serve command if its entry is gone.
These apply to several vendor firmwares; check the equivalents on yours.
- Docker CLI may be off the default PATH — it often lives under the
Docker app's own
bindirectory. Find it withfind / -name docker -type f 2>/dev/null | headand add that directory toPATHin your shell rc. If the daemon is down, start the Docker app in the admin UI and the containers come back. - git may be an alias, not a binary that scripts can call, and may also
live under an app directory.
deploy.shcopes: it reads.git/HEADitself and pulls through analpine/gitcontainer when needed. - Share ACLs strip file modes on clone/pull: run
chmod -R a+rX .in the repo directory afterwards (deploy.shdoes this for you). - SSH auto-block: a burst of SSH connections can blacklist your client IP (ping works, SSH times out). Unblock under the vendor security panel, or connect from your other address (LAN vs tailnet).
| Symptom | Fix |
|---|---|
Build fails in apt-get / pip with a timeout |
Mirror hiccup — rerun ./deploy.sh. |
| Upload rejected with 413 | File exceeds STLTOSOLID_MAX_UPLOAD — raise it in compose environment. |
| Button stuck on "Waiting in queue…" | A previous conversion is still running (they are serialised). Scans take minutes on a small CPU — docker compose logs -f. |
| Conversion dies with no result, container fine | The worker was OOM-killed under mem_limit — raise the limit or convert a decimated mesh. |
| Errors mentioning pymeshlab / Qt on scan uploads | The image installs libgl1 libglu1-mesa libxrender1 libxext6 libsm6 libx11-6 fontconfig libcom-err2 libp11-kit0 libgpg-error0; docker compose exec stltosolid python -c "import pymeshlab" should print nothing. Rebuild with --no-cache if not. |
| UI loads but every API call 404s | Stale image — docker compose build --no-cache. |
| Port already in use | Change the left side of ports: in compose (and the tailscale serve target). |