A browser-native MaterialX graph editor — build and edit MaterialX shading graphs in a browser tab, with a live rendered preview. This image is the application, already behind a web server, ready to run.
It is a static site: no server-side application, no database, no state, and nothing to configure.
docker run -d --name shader-noodles -p 8080:8080 \
nicholasyue/shader-noodles:0.3.0
Open http://127.0.0.1:8080/.
Use 127.0.0.1, not localhost — the port is published on IPv4, while
localhost resolves to the IPv6 ::1 first on many Linux systems, which some
clients will not fall back from.
Pick a material from the menu at the bottom left (marble solid and
simple hair have real graphs to explore), drag a node to move it, drag between
ports to connect them, and click a node to edit its parameters. The preview is
the material itself, generated and rendered live as you edit.
Alongside MaterialX's own examples is lg parquet plank, an oak parquet floor
translated from Larry Gritz's 1995 RenderMan shader parquet_plank.sl into
a MaterialX graph — planks, grooves, growth rings and grain, each with its own
parameter. The pattern follows the original; its lighting is MaterialX's
standard surface rather than RenderMan's plastic. View it on the grid geometry.
Judge it on real geometry. The render view offers a sphere, a grid, a cylinder, a torus and the standard shader ball, each carrying uvs and a proper tangent frame, so anisotropy and normal maps behave as they should rather than as a sphere's parameterisation flatters them. Navigation is what a DCC user expects — Alt+LMB tumble, Alt+MMB pan, Alt+RMB dolly, wheel to zoom, F to frame — and a spin toggle turns the model for judging a highlight in motion. Framing is computed from each geometry's own bounds, so switching never leaves you looking at the inside of something.
"Current browser" is not quite sufficient, and the exception is worth knowing
before you field the report: a browser can decline to give a page WebGL2 even on
a machine whose graphics are working, if its own GPU process is unhealthy —
Chromium-family browsers no longer fall back to software rendering for WebGL on
their own. The app now says so on its start-up screen instead of appearing to
load forever, and names what to check. If you hit it, chrome://gpu is the first
stop, and a different browser is often the fastest answer, since Firefox uses an
independent graphics stack.
Saving depends on the URL you serve it from. The editor can save a .mtlx
file straight back over the file you opened, but browsers only permit that in a
secure context — https://…, http://localhost, or http://127.0.0.1. Over
plain http:// to a hostname or LAN address, Save downloads a copy into the
Downloads folder instead. Nothing is lost and the app says so in its status
line, but it surprises people. Put it behind TLS for any shared deployment.
The image serves plain HTTP on 8080 and terminates nothing by default, on the
assumption that whatever you run it in already has a TLS terminator you manage.
If it does not, there are two supported routes and neither needs a rebuild: mount
your own certificate and a copy of /etc/nginx/tls.conf.example at
/etc/nginx/tls, and a TLS listener appears on 8443; or, if you have no
certificate at all, use the Caddy compose file in the documentation, which
obtains and renews one for you. No certificate ships in this image and none is
generated — a key inside a public image is a key everyone who pulls it holds,
and a self-signed certificate makes browsers warn harder rather than less.
It reaches nothing. No telemetry, no analytics, no update check, no external asset fetch, no absolute URLs anywhere in the bundle. Documents are read and written in the browser, never uploaded. It runs air-gapped, and it will not phone home from inside your network.
First load is ~8 MB (11 MB uncompressed, precompressed here at gzip -9 and
served with gzip_static). Most of that is the MaterialX standard library and
the bundled demo materials. There is a progress indicator; it takes a moment.
The shader ball is a further ~3.6 MB, fetched only when you select it. It is deliberately not in the initial payload — most sessions never open the geometry menu, and carrying it for everyone would make the first load worse for the majority to save one fetch for the minority. Expect a short pause the first time you pick it, once per browser cache.
The image is built to run restricted, and this is the configuration that is tested before each release:
docker run -d --name shader-noodles -p 8080:8080 \
--read-only --tmpfs /tmp \
nicholasyue/shader-noodles:0.3.0
--tmpfs /tmp is required alongside --read-only: the web server needs
somewhere for its scratch paths, and every one of them is under /tmp by
design so that nothing else needs to be writable.
| runs as | uid 101, non-root, declared numerically for runAsNonRoot |
| listens on | 8080 (unprivileged) |
| health | GET /healthz → 200, and a HEALTHCHECK is set |
| base image | nginx:1.27-alpine |
| size | ~76 MB |
| platforms | linux/amd64, linux/arm64 |
Serving is already configured correctly — .wasm as application/wasm,
precompressed assets, and Cache-Control: no-cache so that deploying a new tag
is actually picked up by browsers rather than silently served from cache.
A Deployment and a Service; no operator or chart is needed.
containers:
- name: shader-noodles
image: nicholasyue/shader-noodles:0.3.0
ports: [{ containerPort: 8080 }]
readinessProbe: { httpGet: { path: /healthz, port: 8080 } }
livenessProbe: { httpGet: { path: /healthz, port: 8080 } }
securityContext:
runAsNonRoot: true
runAsUser: 101
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumeMounts: [{ name: tmp, mountPath: /tmp }]
volumes:
- name: tmp
emptyDir: {}
It is stateless, so replicas scale freely and any pod can be killed at any time. The only state a user has lives in their own browser.
:<version> — an exact build, e.g. :0.3.0. Pin this in production.:latest — the most recent release.A published version tag is never re-pointed at different contents: :0.1.0 is
still the build it always was, without the render view. New features arrive as a
new version, so a pin stays worth having.
Every image records exactly which build it is:
docker image inspect nicholasyue/shader-noodles:0.3.0 \
--format '{{index .Labels "dev.shader-noodles.build-id"}}'
# v0.3.0 (7dbe9af)
The same identifier appears in the application's status bar, so a screenshot
identifies its own build. Standard OCI labels (version, revision, created,
licenses, vendor) are set too.
Recorded rather than hidden — these are known, not undiscovered:
<image> nodes will not
render correctly. The bundled demo materials are all procedural.nginx base for reproducibility, which means base-image
security fixes arrive by a new release, not automatically.In the app, press About (right of the status bar) then Copy details —
that captures the build id, the MaterialX and Noodles versions, and which
graphics driver answered, which settles most "it will not render" reports on its
own. For anything involving clicking or dragging, add ?debug=1 to the URL
first, then use the Diagnostics panel at the bottom: Clear, reproduce once,
Copy.
If it never gets as far as the editor, the start-up screen itself reports the reason and offers a Show diagnostics button that opens the same panel. The whole start-up log is also written to the browser's developer console, so it can be copied even from a session that got no further.
This software is proprietary. Copyright © 2026 Nicholas Yue, all rights reserved. Pulling this image does not grant a licence to use, copy, modify, distribute or create derivative works from it. Possession is not permission — use requires a separate written agreement with the copyright holder.
The open-source components built into it (MaterialX, Noodles, Emscripten, RapidJSON, stb, and the bundled fonts) remain under their own licences and are unaffected by the above. Both texts ship inside the image and are served, so you can read them from a running container without unpacking anything:
curl http://127.0.0.1:8080/LICENSE
curl http://127.0.0.1:8080/THIRD-PARTY-NOTICES.txt
Content type
Image
Digest
sha256:4089360a3…
Size
42.1 MB
Last updated
2 days ago
docker pull nicholasyue/shader-noodles