4kr exec --bg gives you a raw detached process keyed only by its PID, a service wraps that same detached-exec mechanism in a managed lifecycle: a stable name, a state file, status and log inspection, restart and stop, an optional public URL, and blue/green deploys with a preview instance.
Use a service for anything that should keep running after the command that started it returns: a web server, an API, a background worker. For one-off or short scripts, plain 4kr exec is enough.
The CLI uses “fork” in some argument help text, while the API and routes use “box”. They refer to the same thing. This page uses “box”.
Mental model
- A service is identified by its box and a service name, both of which must be valid names (lowercase
a-z0-9-). - You manage single services with
4kr servicesubcommands. - A service runs detached on a port inside the box’s pod. It does not get its own DNS name — you reach it through the box.
- Publishing a service attaches an ingress host so the port is reachable at a public URL. Publishing and unpublishing only touch ingress and state; the process keeps running.
- A blue/green deploy starts a second instance (the preview) on a new port behind a preview host, then promotes it to active.
- Service state lives in the box at
/var/lib/forks/services/<service>.json. Service logs are written to/tmp/boxes/exec/<pid>.log.
service status and service inspect report a service as running (the active process is alive) or stopped (the state file exists but no live active process); a service with no state file returns 404 service_not_found. The separate unknown state appears only in service list rows, for a state file that exists but cannot be parsed.
Service vs exec --bg
4kr exec --bg runs a command detached and returns its PID. That is all you get: there is no name, status, log tailing, restart, publish, or preview/promote lifecycle — only a PID.
4kr service start uses the same detached mechanism but adds a named state file, status and log inspection, restart and stop, optional publishing, and blue/green deploys. Reach for a service when you need any of that.
Start a service
--.
Flags:
--port <PORT>— required. The port the process listens on inside the box.--publish <SUBDOMAIN>— optional. Publish the service at this subdomain using the default domain.--domain <DOMAIN>— optional. Override the publish domain.-t, --tag <K=V>— repeatable. Attach tags to the service.-p, --project <P>
The
-t box.ui=main tag is suggested in the CLI help as a marker for the primary web UI. It is a labeling convention only — no behavior is attached to it.-- fails with Error: command is required and exits 1. The API also rejects an empty command, and validates both the box name and service name — an invalid name returns invalid_name.
The server starts the process detached (it survives hangups), records the PID, optionally publishes an ingress host, and writes the service state file.
Inspect services
Status
-j, --json for the raw payload.
Inspect
inspect adds runtime and spec detail on top of status: the active instance’s PID, liveness, port, and command (and the same for any preview), followed by reproducible command hints for start, deploy preview, promote, publish, and unpublish. Pass -j, --json for the raw payload.
List
-j, --json for raw output.
Logs
-l, --lines defaults to 100. If the service has no active process, the command fails with service_no_active_process.
service logs has no --follow and no --json. It returns a single tail of the log.Restart and stop
Restart
service_no_active_process.
Stop
Publish and unpublish
Publishing attaches an ingress host so the service’s port is reachable at a public URL. The process keeps running throughout — publishing only changes ingress and state.--subdomain <SUBDOMAIN>— required. Use@for the apex domain.--domain <DOMAIN>— optional override.--port <PORT>— optional. Override the published port. The default is the service’s active port.-j, --json
--port value, then the active port, then the preview port. If none is available, the call fails with service_port_required. If a preview exists, its host is republished too. On success the response includes the active URL, for example {"ok":true,"active_url":"https://app.example.com"}.
URLs are built as https://<subdomain>.<domain>, except @, which maps to https://<domain> (the apex). The publish domain comes from the configured publish domain unless you pass --domain; an empty domain fails with invalid_publish_config.
Blue/green deploys
A deploy starts a preview instance of the service on a new port behind a preview host, leaving the current active instance serving traffic. Once you have verified the preview, you promote it to active.Deploy a preview
--preview is required; a bare --preview uses the default preview subdomain, and --preview <SUBDOMAIN> overrides it. --port is required, and the command goes after --.If you omit the command, the CLI fails with Error: command is required; if you omit --preview, it fails with Error: --preview is required. The default preview subdomain is <subdomain>-preview, or apex-preview when the published subdomain is @. The preview is published on the new port and stored as the preview instance.Promote the preview
service_not_published if the service is not published, or service_no_preview if there is no preview to promote.Multi-service stacks with compose
4kr compose brings up several services, plus managed dependencies like Postgres or Redis, from a single YAML file. It is the right tool when a box runs more than one service together, or when services need a database or cache alongside them.
up, down, ps, logs.
The compose file
The default file isfork.compose.yaml. Override it with -f, --file.
ComposeSpec):
name— required, non-empty. The stack name.project— optional.base— optional.addons— optional list, default empty.services— required, non-empty map.
ComposeAddon) needs a non-empty use (the addon type) and as (its alias), plus optional fork and hostname.
Each service (ComposeService) needs a required port and a required non-empty command. Optional keys are fork, publish, domain, and env (a map, default empty).
Validation rejects an empty name (compose.name is required), empty services (compose.services is required), any addon missing use or as, and any service with an empty name or empty command.
Addon substitution
You can reference an addon’s outputs in a service’s environment with${addons.<alias>.<key>}:
unterminated ${...} expression, unknown addon alias '<alias>', and unknown addon output '<alias>.<key>'. Interpolated environment is injected by wrapping the command so the variables are exported before it runs.
Addon types and outputs
Each addon type shells out to its managed helper to create the dependency, then exposes outputs you can substitute.- postgres
- redis
- matrix
Use
postgres, postgresql, or pg. Outputs:| Key | Value |
|---|---|
host | <fork>.<project> |
port | 5432 |
user | postgres |
password | postgres |
database | postgres |
url | postgresql://postgres:postgres@<host>:5432/postgres |
use value fails with unsupported addon type '<other>'. An addon’s default fork name is <stack>-<as> unless you set fork on the addon.
What up does
4kr compose up accepts -f, --file, -p, --project, and --force-recreate. It runs in order:
Ensure service boxes
For each unique service
fork (default: the stack name), creates the box if it is missing.Start each service
If a service is already running and you did not pass
--force-recreate, it prints skip <name> (already running). Otherwise it starts the service with the interpolated command and prints started <name>. Compose-started services are started with no tags.up is idempotent: “already exists” and 409 responses are treated as success. --force-recreate re-runs addon creation and service starts, bypassing the running-service skip.
ps, logs, and down
psprints<fork>/<name> <state>per service, where state isrunning,stopped, ormissing(a404for the service maps tomissing). Pass-j, --jsonfor{stack, project, services:[{name,fork,port,state}]}.logsprints a==> <name> <==header before each service’s logs. Pass an optional service name to filter to one service.-l, --linesdefaults to100. There is no--json.downstops each service (best-effort, warning on failures), stops each addon through its helper, deletes the in-box stack state, and printsstopped <name>andstate deleted <project>:<stack>.
/var/lib/boxes/compose/state.json. ps, logs, and down resolve their target in order: an explicit --file, then --stack, then the default fork.compose.yaml if present, otherwise the error compose file not found; pass --file or use --stack.
Managed dependency helpers
Compose addons shell out to standalone helper binaries —forkr-pg, forkr-redis, and forkr-matrix — which you can also run directly to manage a database, cache, or Matrix homeserver in a box. Each takes connection flags (--url, --token, --ip, --insecure, --secure) and resolves the project from --project, then $FORKR_PROJECT, then the git-derived name, then default.
forkr-pg
A Postgres helper. Subcommands:new, fork, connect, start, status, stop, build-base, docs, snapshot, list, delete.
Defaults: user postgres, database postgres, password postgres, port 5432. Data lives in a volume named pgdata-<fork> mounted at /var/lib/postgresql/data.
new <name> creates the data volume and fork, initializes Postgres, waits for the port (--wait-timeout defaults to 60 seconds), and prints ready to connect with a psql command. connect <name> runs a local psql if one is available and the host resolves, otherwise falls back to an in-box console psql. fork <src> <dst> snapshots the source’s data and creates the new fork from that snapshot; the source keeps running.
forkr-redis
A Redis helper. Subcommands:new, fork, connect, status, stop, build-base, docs, snapshot, list, delete.
Unlike
forkr-pg, forkr-redis has no start subcommand.6379, data at /var/lib/redis. AOF persistence is on. Stop performs a shutdown save.
connect <name> uses redis-cli against the host on port 6379. --password is optional, with no default.
forkr-matrix
A Matrix (Synapse) helper with a bundled Postgres. Subcommands:new, fork, publish, new-user, get-access-token, connect-db, status, stop, build-base, docs, snapshot, list, delete.
Defaults: Synapse on port 8080, bundled Postgres on 5432, data under /var/lib/matrix.
new <name> waits for both Postgres (5432) and Synapse (8080). With --hostname it publishes that host and sets Synapse’s server_name and public_baseurl to it; without one, the server is unpublished and reachable only internally. new-user <name> <username> --password <p> [--admin] creates a user (--password is required). get-access-token <name> --user <u> --password <p> returns a token, with --hostname and --url mutually exclusive. publish <name> [<subdomain>] updates Synapse’s public_baseurl; the subdomain is required unless you pass --hostname.
Connecting services
Services do not get their own DNS names. A service listens on a port on its box’s pod, so you reach it through the box’s name and port. Managed dependencies and compose addons form their internal host as<fork>.<project>. A Postgres box orders-db in project shop is reachable at orders-db.shop:5432; a Redis box cache in the same project is cache.shop:6379. Use these <fork>.<project> hosts (as exposed in addon outputs like ${addons.db.host}) to wire services to their dependencies.
Related
Boxes
The Kubernetes-backed sandbox a service runs inside.
Environment
The runtime, filesystem, and DNS your services see inside a box.
Data volumes
Persistent storage that outlives a box — where database state lives.
API reference
The HTTP routes behind
4kr service and 4kr compose.