Package as a unit
Deploying to a host means the workflow lives on a machine you already run. A unit is the other shape: the workflow becomes the machine. You get one file, you install it, and that box does its job with nothing else present.
There is nothing to build. Not a Dockerfile, not a build context — a finished image or a finished Proxmox template, assembled for you.
Build one
On the Workflows screen, tick one or more workflows and choose Package as unit.
Three choices, then Build unit:
| Credentials | Connected (leases from this Nucleus) or Sealed (carries its own, contacts nothing) |
| Format | Docker image, or Proxmox LXC template |
| Access | Token (the unit pins one) or Open (no sign-in at all) |
Everything the selection calls comes with it — the closure of Execute Workflow calls is walked to a fixed point, so a unit never dies at its first sub-workflow call. A workflow you selected starts itself if it has a serving trigger; one pulled in only because something calls it does not, because it is there to be called. Inject and Manual are not serving triggers.
Packaging needs the Manage capability. A sealed unit is an export of the vault in artifact form, so it is gated by the same permission that guards credentials themselves.
It also needs a base artifact on the Nucleus, which a stock install does not carry. If the build fails with “this Nucleus has no docker base artifact”, read Where the base comes from.
Install it
Both artifacts are around 160 MB, because the runtime travels with them.
# Docker
docker load -i orders-pipeline-20260802-1419-linux-amd64.tar
docker run -d --name orders-pipeline --restart unless-stopped \
-p 4801:4801 -v orders-pipeline-data:/data flowdrome-unit/orders-pipeline:20260802-1419
# Proxmox LXC
scp orders-pipeline_20260802-1419_amd64.tar.gz root@pve:/var/lib/vz/template/cache/
pct create 200 local:vztmpl/orders-pipeline_20260802-1419_amd64.tar.gz \
--hostname orders-pipeline --cores 2 --memory 1024 --swap 512 --rootfs local-lvm:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp --unprivileged 1 --onboot 1 --start 1
The dashboard and API are on 4801. The build screen shows the install commands and the access token; both are repeated in the unit’s own README.
Connected or sealed
| Connected | Sealed | |
|---|---|---|
| Credentials in the artifact | none | resolved, real values |
| Needs to reach the Nucleus | yes | no |
| Works air-gapped | no | yes |
| Safe on shared storage | yes | no |
Connected is the ordinary deploy model in a box. Each workflow travels as the usual deploy envelope and the unit leases its 60-second credentials exactly as an enrolled host does. Each packaged artifact registers as its own fleet instance, so you can revoke one unit’s access without touching another copy — and packaging the same unit twice never cuts off the one already running.
Sealed resolves every referenced credential and variable at package time and writes them into the workflow documents. This is deliberately the opposite of the product’s normal rule — a host normally substitutes credentials in memory and never writes them to disk — because an air-gapped box cannot ask anyone for a password. It is opt-in per unit, never the default, the build screen warns before you commit, and the artifact says so about itself.
Rotate a secret and you rebuild the unit. There is no way to push a new value into a box that by definition is not listening.
Access: token or open
Every unit pins its own access token at build time. In token mode that token is required for anything that changes state, and for the contents of a run.
Open the URL the build screen gives you — http://<host>:4801/?token=… — and the browser keeps it.
For the API, send Authorization: Bearer <token>.
There is no username and password. A unit has no Nucleus to sign in against, so the token is the credential.
Browsing without the token is deliberate, and it is not nothing. A visitor who can reach the port sees the unit’s name and environment, whether it is sealed, every workflow it serves with that workflow’s version and routes, and the performance charts — run counts, latency, success rate, slowest steps by name. What the token holds back is run contents and the box’s controls: the charts are served without their run ids, per-run error text or console logs, and the node-by-node run detail, the workflow definitions, the credential names and every start / stop / pause / deploy need the token outright. The endpoint-by-endpoint list is in What a host exposes on its port.
Choosing Open sets no access control at all: anyone who can reach the port can control the unit, read every run’s data and every workflow definition, and enrol it into a Nucleus of their own. It is genuinely convenient on a trusted segment and genuinely dangerous anywhere else, which is why it is a deliberate choice rather than a default.
What the unit tells you about itself
A unit’s dashboard is the host console, scoped to that one box, plus two things units need.
Definition. Every workflow row has a definition action that opens what the box actually runs in the JSON viewer — visual tree or raw code, collapsible, copyable. On an ordinary host you could go and look at the workflow in the Nucleus; on a sealed unit the box is the only copy.
Values that came from credentials read [redacted]. The URL beside the key stays readable — the
point of the view is to see what the workflow does, not to hide it.
Credentials in use. Which credentials this box needs, and which workflows use each. Names only; a value is never shown here or sent by the host. This is the answer to “I am rotating this key — what breaks?”
Credential Used by
dogshop-email Dogshop · Notify customer
dogshop-payments Dogshop · Notify customer Dogshop · Order intake
dogshop-warehouse Dogshop · Order intake
It works on sealed units too, where the documents mention no credentials at all because sealing replaced every marker with its value. The answer is recorded when the unit is packaged, before that happens.
How a unit installs itself
Workflows are baked at /opt/flowdrome-unit and copied into the data root on every boot, before
the host process starts. Three reasons, each of which otherwise produces a box that looks healthy and
does nothing:
/datais routinely a Docker volume or apctmount point, and either one shadows anything baked there. Seeding at boot is correct whether or not a volume is mounted, and whenever it was attached.- The host enumerates its workflow directory once, at startup, with no watcher. Anything that lands a second later is invisible until the next restart.
- The seed prunes workflows the unit no longer contains, so reinstalling a unit over an existing volume cannot leave the previous version’s workflows serving alongside the new ones.
Editing the copies under /data/workflows is therefore not durable — they are refreshed from the
payload at each start. Change the workflow in Flowdrome and package a new unit. Stop and pause state
does survive: it lives in sidecar files the seed never touches.
What cannot be sealed
storage.table and input.table are HTTP clients to the Nucleus Tables API, and a deploy carries a
table’s schema, never its rows. A sealed unit containing one would start cleanly, serve happily,
and fail on its first query — so packaging refuses it with a 409 naming the workflows.
Package those as connected instead, or replace the Tables nodes with data the unit carries.
Where the base comes from
A unit is a shipped base artifact plus your workflows, assembled in managed code. The Nucleus never shells out to Docker — it may itself be running on Windows, in a container, or in an LXC container, none of which has a daemon.
An installed Nucleus does not carry one. The flowdrome/nucleus image and the Nucleus CT
template hold the Nucleus, its UI and the engine worker — not a base — so a stock install cannot
package a unit until you give it one. Packaging works out of the box from a repo checkout that has
already built its artifacts, or on any Nucleus you point at a directory holding them.
It looks for one in three places, in order:
- the directory named by
FLOWDROME_UNIT_BASES, unit-bases/beside the Nucleus executable,dist/docker/ordist/lxc/in any parent directory — which is how a source checkout lays out.
A Docker unit needs flowdrome-host-docker_<version>_amd64.tar (uncompressed — a .tar.gz is
not read; the legacy flowdrome-host-agent*.tar name from older builds is still accepted); an
LXC unit needs flowdrome-host-lxc*.tar.gz. The newest matching file in the first directory
that has one wins. One image build from a source checkout emits both, into dist/docker/ and dist/lxc/.
With none of the three holding a base, packaging answers 409 — “this Nucleus has no docker base
artifact, so it cannot build a finished unit” — and the build screen lists every path it searched
plus the hint to build one or set FLOWDROME_UNIT_BASES. That beats handing you a download that
cannot be installed.
Bases are stamped with the commit they were built from, and packaging refuses a base whose commit
differs from the running Nucleus. That is not fussiness: the base carries the engine worker that runs
your workflows, and one built before a node-type rename will accept every document, report running,
and register zero triggers. Rebuild with scripts/build-docker-images.ps1, which also produces the CT
templates.
Warnings you get at package time
Reported when you build, rather than discovered at 3am inside a container nobody is watching:
- Nodes that need more than the base image — a browser, a C# compiler, XBRL taxonomies.
- Credentials that could not be resolved, which would otherwise reach a live API as the literal
${credential.…}text. - A unit where nothing starts on its own, which is almost always a workflow whose only trigger is an Inject.