Run Formlabs PreFormServer (the headless PreForm with the
Local API)
on Linux. Formlabs ships it for macOS and Windows only; this project runs the
Windows build under Wine, headless, with no GPU, as a Docker container or a
plain user-level install. The point is automation: a Linux box, VM or CI runner
that imports models, orients and supports them, estimates print time, writes
.form files and sends jobs to printers over plain HTTP.
Status: working. The Docker image and a bare Ubuntu 24.04 install both pass
an end-to-end check on every push and every week: import a model, orient,
support, lay out, estimate print time, write the .form file, render a
screenshot, discover a simulated printer by IP and print to it over the network,
all on a GPU-less runner. Printers are reached
by IP address or through a Formlabs account (see Printers); mDNS discovery
and USB printers are the two things Wine does not give you.
git clone https://github.com/mkebiclioglu/preform-linux.git
cd preform-linux
mkdir jobs
docker compose upThat pulls ghcr.io/mkebiclioglu/preform-linux:latest (or builds the same
image locally with docker compose up --build, which is also the fallback if
the pull is refused). The first start downloads PreFormServer from
downloads.formlabs.com (about 330 MB),
verifies Formlabs' Authenticode signature, and stores it in the preform-data
volume, so later starts are quick. The image itself never contains Formlabs
software. When the log shows PreFormServer is ready:
curl http://127.0.0.1:44388/ # version
cp ~/parts/bracket.stl jobs/
curl -X POST http://127.0.0.1:44388/scene/ -H 'Content-Type: application/json' \
-d '{"machine_type":"FORM-4-0","material_code":"FLGPBK05","layer_thickness_mm":0.1}'The container runs PreFormServer as an unprivileged user; set PUID/PGID
(PUID=$(id -u) PGID=$(id -g) docker compose up) so the files it writes to
./jobs belong to you. Files are passed to the API as PreFormServer sees them: the ./jobs directory
is mounted at /jobs in the container, which Wine exposes as Z:/jobs.
So {"file":"Z:/jobs/bracket.stl"} imports it and {"file":"Z:/jobs/bracket.form"}
saves the job back next to it. examples/smoke.sh is a complete
import → orient → support → layout → estimate → save → discover run in bash.
Want a self-contained private image with PreFormServer baked in?
docker build -f docker/Dockerfile --build-arg BUNDLE=1 -t preform-linux:bundled .
(do not publish it; see Licensing).
Needs Wine 11.5 or newer, Xvfb, curl, unzip and osslsigncode. Distro Wine
will not do: PreFormServer 3.63's Qt imports the Windows ICU libraries, which
Wine only gained in 11.5 (March 2026), and Ubuntu 24.04 ships 9.0. Install
WineHQ devel (11.13 or newer runs it as is;
11.5 to 11.12 also need gcc-mingw-w64-x86-64 so the installer can build a
tiny dnsapi.dll shim).
# Ubuntu 24.04: WineHQ devel + the rest
sudo dpkg --add-architecture i386
sudo mkdir -pm755 /etc/apt/keyrings
sudo curl -fsSL https://dl.winehq.org/wine-builds/winehq.key -o /etc/apt/keyrings/winehq-archive.key
sudo curl -fsSL https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources -o /etc/apt/sources.list.d/winehq-noble.sources
sudo apt update && sudo apt install --install-recommends winehq-devel
sudo apt install xvfb osslsigncode
git clone https://github.com/mkebiclioglu/preform-linux.git ~/preform-linux
~/preform-linux/bin/preform-linux install # download, verify, install, init the Wine prefix
~/preform-linux/bin/preform-linux run # foreground; Ctrl-C stops itpreform-linux doctor explains what is missing. systemd/preform-server.service
is a user unit that keeps it running. Paths given to the API are Wine paths:
/home/me/parts/x.stl is Z:/home/me/parts/x.stl.
Three ways to reach a printer from a Linux-hosted PreFormServer, in the order an automation setup should prefer them:
-
By IP address.
POST /discover-devices/with"ip_address"probes one host directly: a TCP connection to the printer's port 35, no mDNS involved, verified to go out under Wine with and without the shim (seeresearch/). Once a printer has answered,POST /scene/{id}/print/takes its serial name or"printer": "10.0.0.21". Give the printers fixed addresses or DHCP reservations (standard practice on a production network anyway) and list them:PREFORM_PRINTERS=10.0.0.21,10.0.0.22 docker compose up
The server probes each one as soon as it is up and again every 10 minutes (
PREFORM_PRINTERS_INTERVAL), soGET /devices/and the MCP'slist_devicesshow them with status, tank and cartridge.preform-linux printers 10.0.0.21(ordocker exec preform preform-linux printers 10.0.0.21) probes on demand and prints the device list. The default Docker bridge network is enough: the container connects out to the printer; the printer never has to connect back. -
Through a Formlabs account. Set
FORMLABS_USERNAMEandFORMLABS_PASSWORD(orFORMLABS_ACCESS_TOKEN; the*_FILEvariants read Docker/Podman secrets) and the server logs in as soon as it is ready. Fleet Control queues and every printer registered to the Dashboard then appear in/devices/and accept jobs, wherever they are. Nothing on the LAN is needed.preform-linux logindoes it on demand. This is HTTPS through Wine; it is wired up but has not been exercised in CI (no account there). -
mDNS discovery: not under Wine. PreFormServer discovers LAN printers with Windows'
DnsStartMulticastQuery, which Wine 11.13+ (and the shim for older Wine) implements as a stub that never answers.POST /discover-devices/without an address therefore returns nothing. The query it makes (captured withPREFORM_SHIM_TRACE=1in CI) is a PTR lookup for_formlabs_formule._tcp.local, so a real implementation inshim/dnsapi.c(Winsock multicast to 224.0.0.251:5353, answer with the PTR/SRV/A records, plus--network hostfor the container) is a well-defined job; contributions are welcome. -
A simulated printer.
sim/printer-sim.pyis a fake printer on TCP port 35 that PreFormServer discovers by IP, lists as a connected Form 4 with tank and cartridge, and prints to: job metadata, the job file and every layer arrive at the simulator and are logged. (A simulated Fuse 1+ or Fuse X1 is discovered and monitored, but SLS jobs are refused pending a firmware-version field that is still being worked out.)PREFORM_PRINTERS=172.28.0.35 docker compose --profile sim upruns it as a sidecar at that address, and the pipeline can be rehearsed end to end, discovery and upload included, before any hardware arrives. Seesim/README.md. The CI does exactly this on every run.
PreFormServer also ships a built-in virtual printer per model (GET /devices/, connection_type: VIRTUAL, ids Form 4, Fuse 1+, Fuse X1, ...);
{"printer": "Form 4"} in a print request runs job generation without any
network. The virtual SLS printers refuse jobs in 3.63; the simulator does not.
Fuse X1. PreFormServer 3.63 (3.63.0 and 3.63.1) prepares Fuse X1 jobs (machine_type
FUSX-1-0, material FLP12G01, layer_thickness_mm 0.11, the one print
setting it ships for the printer): import, print-time estimate and .form export
work, and a simulated Fuse X1 is discovered and monitored. What does not work in
these releases: list-materials leaves the family out, so clients that only trust
that list never see the code (formlabs-local-mcp fills the gap); any other
material or layer height answers Scene type not supported; auto-pack and
auto-layout are refused for this machine type, so models stay where import
puts them; and sending a job to any SLS printer that is not real hardware, the
virtual ones or the simulator, is refused with an "incompatible firmware
version" message. Expect Formlabs to fill those in as the printer ships.
USB printers are out of reach: PreFormServer drives them through a bundled
Windows libusb-1.0.dll and Wine has no USB passthrough. A winelib
libusb-1.0.dll that forwards to the host's libusb would close that gap, but a
server-side automation box next to a printer on a USB cable is rarely the
architecture you want; put the printer on the network.
formlabs-local-mcp 1.0.6
and newer talk to a Wine-hosted PreFormServer directly, mapping file paths for
you. For the Docker image with ./jobs mounted at /jobs:
PREFORM_SERVER_URL=http://127.0.0.1:44388
PREFORM_SERVER_PATH_STYLE=wine
PREFORM_PATH_MAP=/home/me/preform-linux/jobs=Z:/jobs
For a bare-metal install only the first two lines are needed (a Linux path
/home/me/x.stl becomes Z:/home/me/x.stl). Every MCP tool then works as on
macOS: import_model, auto_support, save_form, discover_devices with an
ip_address, print_to_printer with an IP or, after login, a Fleet Control
queue.
- Wine, headless. PreFormServer is a Qt 6 application that needs a display
even without a window, so
runstarts a private Xvfb. Rendering goes through the Mesa software renderer that Formlabs bundles (opengl32sw.dll) viaQT_OPENGL=software; no GPU or GLX is required. - Wine 11.5+. Qt6Core in PreFormServer 3.63 imports
icuuc.dll, the ICU build Windows 10 ships in System32. Wine addedicu,icuucandicuinin 11.5; on anything older the loader stops withSTATUS_DLL_NOT_FOUNDbefore a single line of PreFormServer runs.installandrunrefuse older Wine with a clear message (PREFORM_WINE_UNCHECKED=1overrides, for older PreFormServer releases whose Qt did not use ICU). - The dnsapi problem. Right after opening its HTTP port PreFormServer calls
DnsStartMulticastQuery(Windows mDNS) to look for printers. Wine aborts a process on the first call to a function it does not export, and no Wine release before 11.13 exported this one, so PreFormServer died at "starting HTTP server". Wine 11.13 (July 2026, commitsc13fd5de90and11bca8ddea) added it as a stub that reports success. For older Wine,shim/dnsapi.cis a small DLL that does the same (plus the handful of dnsapi entry points Qt and Wine's own iphlpapi, ws2_32 and netapi32 expect) and is loaded withWINEDLLOVERRIDES=dnsapi=n,b; the installer builds it only when needed. - After "READY FOR INPUT".
runwatches PreFormServer's output; once it is ready it logs in to Formlabs if an account is configured and probes the configured printers, then keeps re-probing on the interval. Passwords are sent to the loopback API from a pipe, never on a command line. - No debugger hangs. The Wine prefix disables
winedbgauto-attach (AeDebug\Auto = 0), so a crash exits instead of parking the process for a debugger that never comes. A server that never reports ready withinPREFORM_STARTUP_TIMEOUTis stopped so a supervisor can restart it. - Clean shutdown.
runtraps SIGTERM/SIGINT, stops PreFormServer, kills the wineserver and its Xvfb. The container usestinias PID 1.
| Variable | Default | Purpose |
|---|---|---|
PREFORM_LINUX_HOME |
~/.local/share/preform-linux (/data in Docker) |
Install root: PreFormServer, Wine prefix, metadata. |
PREFORM_PORT |
44388 |
Port PreFormServer listens on. It binds all interfaces and has no authentication. |
PREFORM_VERSION |
latest |
Pin a PreFormServer release, e.g. 3.63.0. |
PREFORM_PRINTERS |
unset | Printer IPs or hostnames, comma or space separated, probed once the server is up. |
PREFORM_PRINTERS_TIMEOUT |
10 |
Seconds to wait for each probed printer. |
PREFORM_PRINTERS_INTERVAL |
600 |
Seconds between re-probes; 0 probes once. |
FORMLABS_USERNAME, FORMLABS_PASSWORD |
unset | Formlabs account; the server logs in once it is up. |
FORMLABS_ACCESS_TOKEN |
unset | Token alternative to username/password. |
FORMLABS_PASSWORD_FILE, FORMLABS_ACCESS_TOKEN_FILE |
unset | Read the secret from a file (Docker secrets). |
PREFORM_TELEMETRY |
0 |
1 lets PreFormServer send telemetry. |
PREFORM_INSTALL_UNVERIFIED |
0 |
1 accepts a download whose signature cannot be checked (osslsigncode missing). |
PREFORM_SHIM |
auto | 1 always installs the dnsapi shim, 0 never. Auto: only when Wine < 11.13. |
PREFORM_SHIM_DLL |
unset | Prebuilt shim to use instead of compiling (the Docker image sets this). |
PREFORM_SHIM_TRACE |
0 |
1 logs the mDNS queries PreFormServer makes (needs the shim). |
PREFORM_WINE_UNCHECKED |
0 |
1 skips the Wine >= 11.5 check. |
PREFORM_STARTUP_TIMEOUT |
300 |
Seconds to wait for READY FOR INPUT; PreFormServer is stopped if it never gets there. 0 disables. |
PREFORM_XVFB |
auto | 1 always starts Xvfb; 0 uses $DISPLAY. Auto: Xvfb when no DISPLAY. |
QT_OPENGL |
software |
Qt renderer. desktop uses Wine's OpenGL (needs GLX). |
WINEPREFIX, WINEDEBUG, WINEDLLOVERRIDES |
prefix under home, -all, mscoree=d;mshtml=d |
Passed to Wine. |
Docker build arguments: WINE_BRANCH (devel), WINE_VERSION
(11.20~noble-1), PREFORM_VERSION (latest), BUNDLE (0). Container
environment: PUID/PGID (1000) own /data and /jobs; every PREFORM_*,
FORMLABS_*, QT_* and WINE* variable is passed through to PreFormServer.
Image tags: latest and X.Y.Z from releases, main from every green build of
the main branch.
- One container (or unit) per PreFormServer; it is single-tenant and keeps scene state in memory. Scale by running more of them on different ports.
- Put a reverse proxy with authentication in front of the port if anything but localhost must reach it; the API has none of its own.
- Mount the directory your pipeline writes models to at
/jobsand speakZ:/jobs/...to the API. Output.formfiles land in the same place. - Pin
PREFORM_VERSIONand the image tag; the weekly CI run here tells you when a new PreFormServer or Wine release breaks something before you upgrade. - The health check (
GET /answering) turns healthy about a minute after start, later on the very first start while PreFormServer downloads.
- Downloads only from
https://downloads.formlabs.com/PreFormServer/Release/…, with the zip scanned for path traversal before extraction, andPreFormServer.exeverified withosslsigncodeagainst the pinned Microsoft Identity Verification root (certs/) requiring a Formlabs Inc. leaf. A failed check leaves the previous install untouched. - PreFormServer is an unauthenticated HTTP server that reads and writes files
as the user running it. Publish the port on loopback only
(
127.0.0.1:44388:44388, asdocker-compose.ymldoes) or firewall it. In the container it runs as an unprivileged user and only/dataand/jobsare writable and persistent. - Account credentials are read from the environment or a secrets file, sent to the loopback API through a pipe, and never logged.
- Telemetry is off unless you turn it on.
This repository is MIT. It ships no Formlabs code: PreFormServer is fetched from
Formlabs at install time and is covered by the
Formlabs API License Agreement
and the PreForm terms; running it under Wine is your call under those terms. Do
not publish an image built with BUNDLE=1. Not affiliated with or endorsed by
Formlabs Inc.
shellcheck -S warning -x bin/preform-linux docker/*.sh examples/smoke.sh
make -C shim check # needs gcc-mingw-w64-x86-64
docker build -f docker/Dockerfile -t preform-linux:dev .CI (.github/workflows/ci.yml) lints, builds the shim, builds the image with
WineHQ devel and runs examples/smoke.sh against it (including discovery of and
a print to the simulated printer, a print to the virtual Form 4, and the
printers subcommand), repeats the smoke test on a bare Ubuntu 24.04
runner with WineHQ devel and the dnsapi shim forced on with tracing, and
publishes the image to GHCR when everything is green. It runs weekly so new
PreFormServer or Wine releases show up as red rather than as surprises.
See CHANGELOG.md for what changed.
- Real mDNS in the shim (or upstream in Wine) for zero-config LAN discovery.
- A winelib
libusb-1.0.dllbridge for USB printers, if anyone needs one. - arm64 hosts (Raspberry Pi, Apple Silicon without Rosetta) via FEX/box64: unexplored.