# Deploying OpenHTF to a Production Test Station
URL: /guides/production-deployment
Take an OpenHTF test from a developer laptop to a factory station — project layout, pinned dependencies, per-station YAML configuration, launcher scripts and autostart on Windows and Linux, kiosk browser for the Operator UI, record storage and upload, updates and rollback.
OpenHTF has no deployment tool. A station needs Python, your test package, a config file, and something that starts it at boot. Here's the checklist for plain OpenHTF, plus what TofuPilot does for you.
Keep phases and plugs importable as a package so the same code runs from `main.py`, from pytest and from a REPL.
Recreate the environment from this file on every station; never `pip install` interactively on a station. `pip freeze > requirements.lock` on the reference machine gives you the transitive pins for an audit trail. See Installation.
Everything that differs between stations lives in YAML loaded with `--config-file`; the code is identical everywhere.
Declare each key with `CONF.declare(...)` and bind plug constructors with `bind_init_args` — Configuration, PyVISA guide. `python -m pcb01_eol.main --config-help` prints every key a station needs.
Reading `CONF.station_server_port` after the flags are parsed works because `execute()`/`configure()` parse them; if you need the value earlier, call `test.configure()` first or parse with `configuration.ARG_PARSER` yourself (CLI flags).
Naming config files after the hostname means one launcher for the whole line.
`Restart=always` brings the station back after a crash or an operator Ctrl-C. Put secrets in an `EnvironmentFile=` rather than the unit.
Create a task that runs `powershell -File C:\pcb01-eol\scripts\run.ps1` **At log on** of the operator account, with "Run whether user is logged on or not" off (the Operator UI needs a desktop session for the browser) and "If the task fails, restart every 1 minute". Set `TOFUPILOT_API_KEY` as a user environment variable for the operator account.
Alternatively wrap the script with NSSM to run it as a service and start the kiosk browser separately at logon.
`web_launcher.launch()` opens the default browser; on a station you want a full-screen kiosk pointed at the fixed port instead:
Start it from the same launcher (after the server is up) or from the desktop session's autostart, and remove `web_launcher.launch()` from the code. See Operator UI.
Write JSON to a local directory first (`records_dir`); local disk survives network outages.
Send them on with an output callback: a database insert, an S3 sync, or TofuPilot's `upload()`. None of them retry. Keep the local JSON and re-send what failed. Output Callbacks.
Rotate or archive the local directory; a busy station produces thousands of files a month.
Do not inline large attachments (`inline_attachments=False`) if the records are indexed.
Tag releases of the test package (`v2.1.4`) and record the tag in metadata: `htf.Test(..., test_version="2.1.4")`. It lands in every record, so a yield change can be correlated with a test change.
Deploy with `git pull && pip install -r requirements.txt` in the launcher's directory, then restart the service. Rollback is `git checkout v2.1.3` and restart.
Never change limits on a station by hand; change `limits.yaml` in git and redeploy.
The TofuPilot CLI runs the same script, unchanged, and handles these steps:
Step
Plain OpenHTF
With TofuPilot
Ship a new test version
`git pull && pip install`, then restart
Push to Git, stations pull the new build
Roll back
`git checkout v2.1.3`, then restart on every station
Roll back to a previous build
Start at boot
systemd unit or Task Scheduler
Launch on boot
Kiosk browser
`chromium --kiosk` pointed at the station server
Kiosk UI
Records during a network outage
Local JSON, re-sent by your own job
Queued on disk, uploaded when it's back
Your script stays plain OpenHTF, so you can switch back anytime.
`--config-help` on a fresh station lists every key; a missing one fails loudly at start rather than mid-shift.
A `self_test` phase group that talks to every instrument and stops on failure catches unplugged cables before the first unit.
Log to a file (`-v` plus `StandardOutput=append:` or `Start-Transcript`) so the console is not the only record of a crash.