Skip to content

Dorkyfile services

A guide to declaring the services a machine runs. It covers what a flat service is, the keys that express one in a single block (run, bundle, files, packages), the dir shortcut, and how dorky apply --plan shows you the exact set of files that will ship before anything moves.

The file, and the loop

You author one file per machine, named <name>.dorky.toml — for example media.dorky.toml. Three commands make up the loop:

  1. Previewdorky apply media.dorky.toml --plan prints the ship manifest: every file that will travel and where it lands. It is a pure local read of the file and the files beside it, so it works in an empty directory with nothing set up.
  2. Applydorky apply media.dorky.toml provisions the machine if it does not exist yet and converges it to the file.
  3. Reach — the machine answers at https://media.dorky.host. Its name is its subdomain.

A few terms this guide leans on:

  • Machine — the small VM at the network's edge that runs your services. The top-level name in the file (or the filename) names it.
  • Substrate — where a machine runs: fly, unikraft, or a dev sprite. Omit [provider] to take the default, or pin it with a [provider] table.
  • <home> / $DORKY_HOME — the machine's home directory, the root everything you ship lands under.

On the machine, your <name>.dorky.toml is retained as dorkyfile.toml. That is the name you see for it in every ship manifest.

What a service is

A [[service]] block declares one long-running process (or a set of static files) the machine serves. A machine's Dorkyfile can hold as many as you need. The machine hosts them side by side, each under its own working directory at <home>/services/<name>/.

There are two ways to write a service, and they mean the same thing:

  • Flat form — every field inline in the [[service]] block. This is the plain way to write a service, and everything else is built on it.
  • The dir shortcutdir = "./x" points at a folder that carries its own service.toml. The machine reads that folder's service.toml and fills in the same fields the flat form spells out.

If you are starting fresh, write the flat form. Reach for dir when a service is already a self-contained folder you want to keep together.

The flat form, key by key

toml
[[service]]
name     = "downloader"                       # required for a flat service
run      = "bun ${DORKY_SERVICE_DIR}/app.js"  # the start command, run verbatim
port     = 8813                               # the port the process listens on
bundle   = "./app.ts"                         # optional: build one .js from this entry
files    = ["assets/*.png"]                   # optional: ship files alongside the process
packages = ["ffmpeg"]                         # optional: tools this service needs on PATH
routes   = { "/download" = "token" }          # optional: paths this service owns
  • name is the service's name, and the folder it lives in on the machine. A flat service must name itself; leave it out and the apply stops at parse with a message telling you to add it.
  • run is the command that starts the process, written and executed verbatim by the machine's shell. Reference the service's own directory as ${DORKY_SERVICE_DIR} and its listen port as ${DORKY_<NAME>_PORT}. A service with a run command needs a valid port, and run should be a process that stays up (a server), since the machine relaunches a service that exits.
  • port is the port the process listens on. The machine exports it and routes to it.
  • bundle builds one file from a TypeScript entry (see Shipping code).
  • files ships files alongside the process (see Shipping files).
  • packages names tools the service needs on PATH (see Declaring tools).
  • routes are the paths this service owns and how each is gated (public, token, owner, byo-validator). A service with no run is a static service: it serves files or routes and runs no process.

Reading the port

The machine assigns each service its listen port and exports it as an environment variable named after the service: DORKY_<NAME>_PORT, where <NAME> is the service name uppercased with every - turned into _.

Service namePort variable
downloaderDORKY_DOWNLOADER_PORT
siteDORKY_SITE_PORT
upstream-gatewayDORKY_UPSTREAM_GATEWAY_PORT

Your process reads that variable and listens on it. A Bun service:

ts
const port = Number(process.env.DORKY_DOWNLOADER_PORT);
Bun.serve({ port, fetch: () => new Response("ok\n") });

Or straight in the run command, when the program takes the port as an argument:

toml
run = "python3 -m http.server ${DORKY_SITE_PORT}"

The smallest service is a name and a run command — no code folder, no bundle, no files:

toml
[[service]]
name     = "clock"
run      = "python3 -m http.server ${DORKY_CLOCK_PORT}"
port     = 8814
packages = ["python3"]

The machine creates an empty working directory at <home>/services/clock/, exports the port, resolves the python3 package onto PATH, and starts the command. Nothing is shipped; the process is all there is.

Shipping code (bundle)

bundle points at a TypeScript entry file, relative to the Dorkyfile:

toml
[[service]]
name   = "downloader"
run    = "bun ${DORKY_SERVICE_DIR}/downloader.js"
bundle = "./downloader.ts"
port   = 8813

When you apply, the CLI builds that entry with bun build and ships only the single output .js into the service's folder. The compiler inlines the entry's whole import graph into that one file. Your source .ts files, package.json, lockfiles, and node_modules never travel. This is the answer for a service that lives in a larger repo: you ship one built file, not the repo.

The output filename comes from your run command (the .js it launches); if run names none, it is the entry's name with .ts swapped for .js. The entry path must be relative to the Dorkyfile.

Shipping files (files)

files ships named files and globs. Entries are paths or globs relative to the Dorkyfile's directory, and each matched file lands at the same relative path under the scope's root.

  • Machine scopefiles at the top level of the Dorkyfile. Lands under <home>/files/.
  • Service scopefiles inside a [[service]] block. Lands under that service's own folder, <home>/services/<name>/.
toml
name  = "studio"
files = ["samples/*.wav"]

[[service]]
name  = "site"
run   = "bun ${DORKY_SERVICE_DIR}/serve.js"
port  = 8080
files = ["serve.js", "web/index.html"]

Here the machine-scope samples/*.wav land under <home>/files/, while the site service ships both the code its run launches (serve.js) and the page it serves (web/index.html) into its own folder. Notes:

  • Ship the file your run launches. Nothing is shipped implicitly, so a run that starts serve.js only works if serve.js is listed in files (or built by bundle).
  • Globs expand on your computer at apply time. The bundle carries concrete files; what you see in the ship manifest is exactly what travels.
  • The path is mirrored, never renamed. samples/kick.wav lands at files/samples/kick.wav.
  • A wildcard never ships junk. .git, node_modules, any .env or *.env, and OS debris are dropped even when a greedy glob would match them, so a secret cannot enter through a wildcard. Secret values arrive only through dorky secret set, never a file.
  • A literal that matches nothing is an error; an empty glob is allowed. Absolute paths and paths that escape the Dorkyfile's directory are rejected.

Declaring tools (packages)

packages names the command-line tools a service needs on its PATH. A machine has one environment shared by all its services. So per-service packages fold up into that one environment: the machine resolves the union of every service's packages plus any declared at the top level.

These two Dorkyfiles produce the same environment:

toml
# declared per service — reads next to the code that needs it
packages = ["yt-dlp"]

[[service]]
name = "transcriber"
run  = "uv run transkun ..."
packages = ["transkun", "ffmpeg"]
toml
# declared once at the top — equivalent
packages = ["yt-dlp", "transkun", "ffmpeg"]

Declaring a package next to the service that needs it keeps the Dorkyfile readable; the machine merges them either way. Resolution happens at apply time against the machine's own system, so --plan names the packages but does not resolve them.

The dir shortcut

dir = "./x" is a shortcut for the flat form. The machine reads ./x/service.toml and fills in the flat fields from it; anything you also write inline in the [[service]] block wins over the harvested value.

toml
[[service]]
dir = "./services/upstream-gateway"

is equivalent to a flat block whose name, run, port, secrets, and routes come from ./services/upstream-gateway/service.toml, with the folder shipped as the service's on-machine tree. The flat form is the canonical expression; dir is the sugar, and you never have to use it.

Serving static files

A service with no run command is static: it serves files or routes and runs no process. Point a route at a directory with dir, and the machine serves that directory with a file server. This is how docs.dorky.host — the site you are reading — serves itself:

toml
[[service]]
name  = "docs"
files = ["www/docs.dorky.host/dist/**"]

[service.routes]
"/" = { mode = "public", dir = "www/docs.dorky.host/dist" }

The files glob ships the built site into the service's folder, and the route roots a public file server at that directory. No process runs, so a static-only machine needs no runtime.

Previewing what ships — dorky apply --plan

Before anything ships, dorky apply <file> --plan (also spelled --dry-run) prints the ship manifest: every file that will travel, where it lands on the machine, and its size. It reads only the Dorkyfile and the files beside it, so it needs no machine and nothing configured. This is how you confirm a glob expanded to what you expected, before the fact.

For the studio example above, previewing prints:

plan    studio.dorky.toml → "studio"
  bundle  dorkyfile.toml, site (files → services/site/), files → files/
  ship    5 files:
            dorkyfile.toml                167 B
            files/samples/kick.wav        13 B
            files/samples/snare.wav       14 B
            services/site/serve.js        194 B
            services/site/web/index.html  31 B

Every landed byte traces to a files, bundle, or dir reference you wrote. Nothing ships implicitly, and there is no "copy the current directory" path. The file set and its landing paths are the invariant to check: sizes come from a reference run and track your own file bytes.

Quick reference

KeyScopeMeaning
nameserviceThe service's name and folder. Required for a flat service.
runserviceStart command, run verbatim. Needs a port; should stay up.
portserviceThe listen port, exported as DORKY_<NAME>_PORT.
bundleserviceA TS entry (Dorkyfile-relative) built to one .js.
filesmachine + servicePaths/globs (Dorkyfile-relative), path-mirrored under the scope root.
packagesmachine + serviceTools on PATH; service-scope folds up into the machine's one environment.
routesserviceThe paths a service owns and how each is gated.
dirserviceShortcut: harvest a folder's service.toml into the flat form.

Landing paths: machine files<home>/files/; a service's tree → <home>/services/<name>/. Preview any apply with dorky apply <file> --plan — no machine required.