A fast, single-binary process runner for local dev stacks.
Run your entire development stack from one YAML file — correct startup order, restart policies, file-watch reload, one merged readable log, and (optionally) a PM2-style background instance you can check on and control from any terminal.
No containers. No Procfile glue. No unnecessary complexity.
stark up
Table of contents
Why
Local dev stacks are usually a handful of long-running processes — an API, a frontend dev server, a database, a queue — started by hand across a handful of terminal tabs, or wrapped in a Makefile/shell script that doesn't handle restarts, startup order, or merged logs. Stark runs all of it from one stark.yaml: correct startup order via depends_on, restart policies, file-watch-triggered restarts, one merged readable log stream, and (optionally) a background instance you can check on and control from any terminal.
This project is under active early development — see Commands below for what works today.
Install
With Go installed:
go install puts the binary in $(go env GOBIN) (or $(go env GOPATH)/bin if GOBIN isn't set) — that directory needs to be on your PATH before stark is callable by name. One-time setup, if you haven't already:
Linux / macOS (bash or zsh)
echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
source ~/.bashrc # or open a new terminal
Windows (PowerShell)
$goBin = go env GOPATH
[Environment]::SetEnvironmentVariable("Path", "$env:Path;$goBin\bin", "User")
Open a new terminal afterward for the change to take effect.
Then install:
go install github.com/starkbaknet/stark/cmd/stark@latest
Pin a specific version with @v0.0.1 instead of @latest if you want reproducible installs.
Prebuilt binary, no Go needed: every tagged release publishes linux/darwin/windows × amd64/arm64 binaries, with checksums, on the Releases page.
curl -L -o stark.tar.gz https://github.com/starkbaknet/stark/releases/latest/download/stark_<version>_<os>_<arch>.tar.gz
tar xzf stark.tar.gz
sudo mv stark /usr/local/bin/stark
(swap <version>_<os>_<arch> for e.g. 0.0.1_linux_amd64; Windows ships as a .zip instead)
From source:
git clone https://github.com/starkbaknet/stark.git
cd stark
make build # if you have make — also injects version/commit/date into the binary
go build -o stark ./cmd/stark # otherwise (stark version will report dev/none/unknown)
Confirm whichever way you installed it:
stark version
Quick start
Create a stark.yaml:
version: 1
services:
api:
command: go run ./cmd/api
workdir: backend
watch:
- cmd
- internal
restart: on-failure
web:
command: npm run dev
workdir: frontend
depends_on:
- api
redis:
command: redis-server
stark validate
stark up
stark up starts api (and redis, independent of it) before web, since web depends_on: [api] — streaming every service's output to your terminal, tagged and colorized by name, until you hit Ctrl+C.
See Examples below for runnable, dependency-free demos of every feature this README describes.
Configuration
stark.yaml — one version and a map of named services:
| Field |
Type |
Required |
Default |
Notes |
version |
int |
yes |
— |
must be 1 |
services.<name>.command |
string |
yes |
— |
run directly as argv — never through a shell, so no &&/|/$VAR shell expansion |
services.<name>.workdir |
string |
no |
. |
resolved relative to the config file's own directory (not wherever you run stark from); must exist |
services.<name>.env |
map[string]string |
no |
{} |
merged over a minimal inherited environment (PATH, HOME, and a few OS-required variables) — not your full shell environment |
services.<name>.watch |
[]string |
no |
[] |
directories (resolved the same way as workdir) that trigger a restart on change; new subdirectories are picked up automatically, and a burst of changes is debounced into one restart |
services.<name>.depends_on |
[]string |
no |
[] |
must reference other services in the same file; the full graph must be acyclic |
services.<name>.restart |
never | on-failure | always |
no |
never |
see Restart policies |
A few things worth knowing:
- Strict parsing. An unrecognized field anywhere in the document — a typo'd name, a field indented under the wrong parent — is a load-time error, not a silently ignored key.
- No shell.
command is tokenized into argv directly (quoting works the way you'd expect; no globbing, no variable expansion, no command substitution). If a command needs shell features, wrap it explicitly: command: sh -c "foo && bar".
- No anchors/aliases. YAML
&name/*name (including merge keys <<: *name) are rejected outright — nothing in this schema needs them, and it closes off a real amplification surface some YAML parsers otherwise tolerate from a small, adversarial document.
workdir-relative commands work. command: .venv/bin/python manage.py runserver with workdir: backend resolves the interpreter relative to backend/, not wherever you invoked stark from — handy for a Python virtualenv or any per-project toolchain that isn't on PATH.
Every validation error names the service, the field, what was found, and what was expected:
config error: services.api.restart: invalid value "on-fail" (expected one of: never, on-failure, always)
Commands
Every command below also has a full man-page-style reference built into the binary — see stark docs.
A general rule for every command that takes both flags and a positional argument (a service name): flags must come before the positional. stark restart -config path api works; stark restart api -config path does not (fs.Parse stops at the first non-flag argument).
stark up
stark up [-config path] [-c path] [-detach] [-d] [service]
Loads and validates the config, computes a start order from depends_on, then starts every service in that order under a restart-policy supervisor. Each service with a non-empty watch restarts on file change. Output is merged onto one stream, each line tagged [name] and colorized per service in a real terminal.
- Foreground (default): runs until Ctrl+C/SIGTERM, then stops every service in reverse dependency order (a grace period, then a force-kill).
-detach/-d: spawns a background instance instead, returns your shell as soon as every service has started (or reports why it didn't), and exits immediately. See Background mode.
[service]: starts only that service and everything it transitively depends_on — see Running a single service.
stark up
stark up -d
stark up api
stark up -d api
stark up -config examples/hello-server/stark.yaml
stark stop
stark stop [-config path] [-c path] [service]
Without a service name: stops the whole background instance — every service in reverse dependency order, log files closed, running-instance state removed — and waits for that to actually finish before printing stark: stopped. Prints stark: not running (exit 0) if nothing was running.
With a service name: stops only that service; the daemon and every other service keep running. Marks it as intentionally stopped, so its restart policy won't bring it back on its own — bring it back explicitly with stark restart <service> or stark up -d <service> (both start it fresh, resetting its restart count).
stark stop
stark stop worker
stark ps
stark ps [-config path] [-c path]
stark ps --all
Lists this project's services — name, pid, status, restart count, uptime. Errors if nothing is running (stark up -d to start one).
--all ignores -config and instead scans for every project with a running instance anywhere on the machine, in one table with an extra PROJECT column — no persistent central registry daemon involved, it just scans the state root on demand.
stark ps
stark ps --all
stark restart
stark restart [-config path] [-c path] [service]
Restarts the named service immediately, bypassing its restart policy and crash-loop budget — the same mechanism a file-watch change uses. No service name restarts every currently-running service. Nothing running: errors if a service name was given, otherwise starts the whole stack detached — matching pm2 restart's behavior when nothing is running yet.
Restarting a service that was stopped individually with stark stop <service> starts it fresh (its restart count resets), rather than failing.
stark restart
stark restart api
stark logs
stark logs [-config path] [-c path] [service]
Prints the last 200 matching lines from the background instance's combined log, then follows it (polling every 300ms) until interrupted. A service name filters to just that service's lines. The log file on disk is always plain text; color (matching up's tags) is applied only at display time, in a real terminal.
stark logs
stark logs web
stark validate
stark validate [-config path] [-c path]
Runs every check up would — config load, defaults, path resolution, full semantic validation — without starting anything. Prints <path>: valid on success.
stark validate
stark version
stark version
Prints version, commit, build date, OS/arch, and Go runtime version — e.g. stark 0.0.1 (a1b2c3d) built 2026-07-08T12:00:00Z linux/amd64 go1.26.4. A binary built without -ldflags reports dev/none/unknown for the first three.
stark docs
stark docs
stark docs <command>
stark <command> docs
Man-page-style reference documentation, embedded in the binary via go:embed — works offline, always matches the exact version you're running, no separate install. Bare stark docs prints an index of every command; stark docs <command> or stark <command> docs (either order works — the latter is handled centrally, not duplicated per command) prints that command's full page.
Two extra reference pages exist only through docs, not as commands of their own:
stark docs config # the stark.yaml field reference
stark docs architecture # a tour of the codebase's internal packages
docs is a reserved trailing argument for every command — stark restart docs shows restart's docs page rather than restarting a service, even if you have a service literally named docs.
Running a single service
Every up invocation, foreground or detached, accepts an optional service name:
stark up api # foreground: api + whatever it depends on, nothing else
stark up -d api # same, detached
Only the named service and its transitive depends_on closure start — everything else in the config is left out entirely, as if it weren't there. A service that depends on the named one (but isn't depended on by it) never starts.
-d <service> against a project that already has a background instance running doesn't spawn a second, competing one — it asks the existing instance to start that service instead (resurrecting it, with a reset restart count, if stark stop <service> had stopped it individually). This only works for a service the running instance already knows about — a service that was never part of what that instance was originally started with still needs stark stop (whole instance) + a fresh stark up -d.
Background mode
stark up -d detaches into the background, the same way pm2 start does — your shell gets control back as soon as every service has started, and the stack keeps running independently of that terminal.
Each project's running instance is tracked by a small amount of state kept outside your repo, under a per-user directory (mirroring PM2's ~/.pm2):
~/.stark/<project>-<hash>/ (%LOCALAPPDATA%\stark\<project>-<hash>\ on Windows)
meta.json # pid, project path, start time
logs/
combined.log # every service's output, tagged by name — what `stark logs` follows
stark.log # Stark's own status messages (started/stopped/restarted)
Override the base directory with the STARK_HOME environment variable (same idea as PM2's PM2_HOME) if you don't want ~/.stark.
stop/ps/restart/logs/up -d <service> all resolve which instance to talk to from the config path (-config, same as up), so run them from the project directory (or pass -config) the same way you'd run up.
A project has exactly one background instance regardless of how it was started — you can't have one -d run backgrounding one subset of services and separately background a completely different, non-overlapping one. Whatever the instance was started with (whole config, or grown via later up -d <service> calls) is the instance until you stop it entirely and up -d again.
Talking to a running instance goes over a loopback-TCP control channel, authenticated with a random per-instance token written alongside meta.json — works identically on every OS, no named pipes or Unix sockets to special-case.
Colored output
stark up's merged output, its own status lines, stark ps, and stark logs all colorize per service — the same service name always gets the same color, consistently across all three. Color is applied only when the destination is a real, attached, color-capable terminal:
- Piped or redirected to a file (
stark up | cat, stark up > out.log) → plain text, byte-for-byte, always.
combined.log/stark.log on disk are never colorized regardless of how the background instance's own terminal was configured — stark logs recolors them at display time only.
- Set
NO_COLOR=1 to force plain output even in a real terminal (no-color.org).
Restart policies and the crash-loop budget
| Policy |
Behavior |
never (default) |
Never restarts, regardless of exit code. |
on-failure |
Restarts only on a non-zero exit code or signal death. |
always |
Restarts even on a clean exit. |
Restarts use exponential backoff (200ms base, capped at 5s). A service that restarts 5 times within 10 seconds has exceeded its crash-loop budget — up gives up on it, stops the rest of the stack, and returns an error, rather than spinning forever unnoticed.
A file-watch-triggered restart (from watch:) and a manual stark restart <service> both bypass the restart policy and the crash-loop budget entirely — they're deliberate, not crash recovery.
Examples
Every example under examples/ except basic/ needs only the Go toolchain — no npm, no Redis, nothing else to install.
| Example |
What it's for |
hello-server/ |
The smallest possible demo — stark up, a restart policy, and file-watch reload with a single service. |
multi-service/ |
Three services wired with depends_on — dependency start order, and colored, per-service tagged merged log output. |
restart-policies/ |
All three restart: policies side by side, including the crash-loop budget tripping and taking the whole stack down. |
env-workdir/ |
How workdir resolution and env merging actually behave, made visible instead of just described. |
daemon-lifecycle/ |
The full background-mode walkthrough: up -d, ps, logs, restart, stop, ps --all. |
basic/ |
Illustrates the full config shape (a Go API + npm frontend + Redis); needs go, npm, and redis-server on PATH to actually validate/run. |
FAQ
Why not Docker Compose? Compose is a container orchestrator; Stark runs your actual local toolchain (go run, npm run dev) directly, with no image build step.
Why not a Procfile runner (foreman, overmind, goreman)? Those are close cousins. Stark adds first-class depends_on ordering, restart policies, and file-watch-triggered restarts as config, not shell glue.
How is background mode different from PM2? It's deliberately smaller in scope: one independent instance per project (keyed by your stark.yaml's path) rather than PM2's single persistent daemon managing a central app registry — stark ps --all gets you the cross-project view by scanning on demand, but there's no clustering, load-balancing, or general-purpose app management. If you need to manage many unrelated long-running apps across a whole machine, PM2 is still the right tool for that; Stark's background mode is for not having to babysit this project's dev stack in a dedicated terminal tab.
Does Stark support multiple instances / clustering of one app, like pm2 start -i? No, and it's not planned — that's PM2's actual niche. Stark runs each configured service once.
My config's command isn't found, but the binary is definitely there. If it's a relative path (e.g. .venv/bin/python), it resolves relative to that service's workdir, not wherever you ran stark from. If it's a bare name (e.g. python), it's resolved via PATH — check it's actually on PATH in the minimal environment Stark inherits (see services.<name>.env above).
Contributing
See CONTRIBUTING.md.
License
MIT