Skip to content

devlanes is in beta and not ready for production use.Report an issue

Config reference

Every field of devlanes.yaml and of the per-user config.yaml, generated from the JSON Schemas devlanes publishes. In a path, <name> stands for a key you choose (a service, task or profile name) and [] for an item of a list. A type marked templated also accepts a ${{ ... }} expression, evaluated for each lane. A duration is a number with ms, s, m, h or d, and a scalar is a string, number or boolean. examples/devlanes.yaml shows the fields in context.

devlanes.yaml at the repository root or in a subdirectory; devlanes uses the nearest one above the current directory, up to the repository root. Another project file in the same directory, such as devlanes.prod.yaml, is named once with --file. Each project file can have local overrides in a .local.yaml next to it. Schema: https://devlanes.run/v1/schema.json.

PathTypeDescriptionExample
extendsstring or list of stringOther project files this one builds on: one path or a list, relative to this file, such as devlanes.yaml for a devlanes.prod.yaml that runs the same apps another way. They are merged in order, each with its own .local.yaml, then this file and its .local.yaml go last, so later files win. Mappings merge by key and lists and single values replace, as with local overrides. An extended file may be partial and its project is ignored; paths in it mean what they would in this file.devlanes.yaml
PathTypeDescriptionExample
global_envmap of scalar (templated)Variables for every service and task of the lane, above devlanes’ own variables (PORT, DEVLANES_*) and below each unit’s env and the profile’s env. Use it for values many units share, such as a database URL built from ${{ infra.postgres.port }}. Never set PORT here: it would replace every service’s own port.{"API_URL":"${{ services.api.url }}"}
PathTypeDescriptionExample
infraobjectContainers the lanes depend on (databases, brokers, object stores), started and waited on before setup, tasks and services, plus the commands that create and remove each lane’s data in them.{"compose":"docker-compose.yml","hidden":["postgres"]}
infra.composestring or list of stringCompose file, or a list of files merged like repeated -f, relative to the project. Its services are shared by every lane unless listed in per_lane; the shared project runs from the primary worktree with that worktree’s .env as --env-file.docker-compose.yml
infra.containersmap of objectDocker containers run without a Compose file, keyed by name and reached as infra.<name> in expressions.{"cerbos":{"image":"ghcr.io/cerbos/cerbos:0.40.0","per_lane":true,"ports":{"http":3592}}}
infra.containers..commandstring (templated) or list of string (templated)Command replacing the image’s default: a list is used as is, a string runs through /bin/sh -c inside the container.["server","--config=/policies/config.yaml"]
infra.containers..envmap of scalar (templated)Environment variables set inside the container.{"LOG_LEVEL":"debug"}
infra.containers..imagestring (templated)Image to run. Required. A running container with the same image, command, env, mounts and ports is reused; any change recreates it.ghcr.io/cerbos/cerbos:0.40.0
infra.containers..per_laneboolean (templated)Run one container per lane instead of one shared by every lane. Defaults to false. Per-lane containers and their named volumes are removed by devlanes lane clean.true
infra.containers..portsmap of integer (templated)Container ports by name. devlanes publishes each on a host port on 127.0.0.1, read as ${{ infra.<name>.ports.<port> }}; infra.<name>.port is the first one.{"grpc":3593,"http":3592}
infra.containers..readystarted or objectExtra check the start waits for once the container is up (the image’s healthcheck, else its first port accepting TCP), before setup, tasks and services run. Write started or one of tcp, http, log (matched against the container’s logs) or cmd (run on the host). If it does not pass within its timeout (default 60s), the start fails. Values: started: Count the unit ready as soon as it runs, without probing anything. A dev server whose program crashed under a file watcher still counts as ready, so prefer a check when there is something to probe.started
infra.containers..ready.cmdstring (templated) or list of string (templated)Ready once this command exits 0. It runs on the host through /bin/sh (a list is quoted into one command line) with its output discarded, is retried until the timeout, and each attempt is cut off after 10 seconds. For a service it gets the service’s environment and working directory.curl -fsS ${{ self.url }}/healthz
infra.containers..ready.httpobjectReady once a GET request to path on the unit’s main port answers with status, or with a 2xx status when status is unset. Redirects are not followed. Best when the app has a health route that fails while it is broken. Each request sends User-Agent: devlanes-ready, so an app can leave the checks out of its request logs.{"path":"/healthz","status":200,"timeout":"60s"}
infra.containers..ready.http.hoststring (templated)Host header the check sends, for apps that answer only on their own hostname, such as ${{ self.hosts[0] }}. The request still goes to 127.0.0.1; only the header changes. Defaults to 127.0.0.1:<port>. Spaces and control characters are refused.${{ self.hosts[0] }}
infra.containers..ready.http.pathstring (templated)Path requested with GET, such as /healthz; defaults to /./healthz
infra.containers..ready.http.statusinteger (templated)Status code that counts as ready, between 100 and 599; only that exact code passes. When unset, any status from 200 to 299 counts, so a 404 or a redirect keeps waiting (redirects are not followed). Set it to accept another code, such as 302 for a route that redirects to a login.200
infra.containers..ready.http.timeoutduration (templated)How long the HTTP check may take to pass, replacing the check’s timeout; defaults to 60s. When it runs out, a service is marked failed but keeps running, and for an infra container the start fails.15s
infra.containers..ready.logstring (templated)Ready once a line of output matches this regular expression, with colors and other terminal escape codes removed; recent lines printed before the check began count too. Suits workers without a port: have them print a line once connected.Connected to Redis
infra.containers..ready.tcpboolean or stringReady once a port accepts a TCP connection on 127.0.0.1 or ::1, tried every 150 ms. true uses the unit’s main port; a port name such as inspector uses that named port. It only proves the socket is open, not that the app works.true
infra.containers..ready.timeoutduration (templated)How long the check may take to pass; defaults to 60s. When it runs out, a service is marked failed (its process keeps running and is not restarted) and after: services tasks are skipped; for an infra container the start fails. Raise it for services that compile before they listen.15s
infra.containers..volumeslist of string (templated)Mounts as source:destination[:mode]. ./relative and /absolute sources are bind mounts, relative ones from the owning worktree; a bare name is a named volume kept per project or per lane.["./infra/cerbos:/policies:ro"]
infra.envmap of scalar (templated)Variables for ${VAR} interpolation in the Compose files only; processes do not see them (use global_env for those).{"ACME_DB_PORT":15432}
infra.excludelist of stringCompose services devlanes never starts, such as a local reverse proxy the devlanes proxy replaces.["caddy"]
infra.externalmap of objectServers you run yourself on a loopback port, such as pgAdmin, keyed by name. Every lane lists them, the proxy routes their hostnames, and machines connected with devlanes remote connect mirror them while a lane of the project runs. devlanes never starts or checks them. Put personal ones in devlanes.local.yaml.{"pgadmin":{"address":"127.0.0.1:5050","hosts":"pgadmin.${{ lane.domain }}"}}
infra.external..addressstringWhere the server listens, 127.0.0.1:<port> or [::1]:<port>. Only loopback addresses work. A port that devlanes or a lane already uses is an error. Required.127.0.0.1:5050
infra.external..hiddenbooleanKeep the entry away from machines connected with devlanes remote connect. They never see, mirror or reach it. This machine still routes its hostnames. Defaults to false.false
infra.external..hostsstring (templated) or object or list of string (templated) or objectHostnames the devlanes HTTPS proxy routes to the port. One entry or a list. An entry is a hostname, or a mapping with host and optional fallback and host_header. Expressions can use lane, project, infra and env. Put the names under ${{ lane.domain }} so each lane gets its own. The first host without a wildcard becomes the unit’s URL.pgadmin.${{ lane.domain }}
infra.external..hosts.fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Defaults to false.true
infra.external..hosts.hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself. Quote it in YAML. Labels hold letters, digits and inner hyphens. Case and a trailing dot are ignored.pgadmin.${{ lane.domain }}
infra.external..hosts.host_headerpreserve or upstreamHost header the server receives. Defaults to preserve, the hostname the browser asked for. upstream sends the address devlanes connects to, such as 127.0.0.1:5050, for servers that only accept their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
infra.external..hosts[].fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Defaults to false.true
infra.external..hosts[].hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself. Quote it in YAML. Labels hold letters, digits and inner hyphens. Case and a trailing dot are ignored.pgadmin.${{ lane.domain }}
infra.external..hosts[].host_headerpreserve or upstreamHost header the server receives. Defaults to preserve, the hostname the browser asked for. upstream sends the address devlanes connects to, such as 127.0.0.1:5050, for servers that only accept their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
infra.hiddenlist of stringContainers that machines connected with devlanes remote connect never see, mirror or reach.["postgres"]
infra.hostsmap of string (templated) or object or list of string (templated) or objectHTTPS hostnames for containers, keyed by Compose service or plain container name, so a browser can open a container’s web port: a MinIO console or presigned S3 URLs, Mailpit, a mock OIDC provider. A hostname routes to the container’s first published port unless its route sets port; each key takes one hostname or route, or a list.{"s3mock":"s3.${{ lane.domain }}"}
infra.hosts..fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for *.${{ lane.domain }} on a gateway that should answer every subdomain other services do not claim. Defaults to false.true
infra.hosts..hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored.api.${{ lane.domain }}
infra.hosts..host_headerpreserve or upstreamHost header the unit receives. Defaults to preserve, the hostname the browser asked for; upstream sends the local address, such as 127.0.0.1:<port>, for servers that reject or misroute requests for a Host that is not their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
infra.hosts..portstringSend this hostname to one of the unit’s named ports instead of its main port. For a service it is a port name as in self.ports.<name>, and naming it here allocates that port; in infra.hosts it is the container port for Compose services, or the port’s name for plain containers.inspector
infra.hosts.[].fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for *.${{ lane.domain }} on a gateway that should answer every subdomain other services do not claim. Defaults to false.true
infra.hosts.[].hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored.api.${{ lane.domain }}
infra.hosts.[].host_headerpreserve or upstreamHost header the unit receives. Defaults to preserve, the hostname the browser asked for; upstream sends the local address, such as 127.0.0.1:<port>, for servers that reject or misroute requests for a Host that is not their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
infra.hosts.[].portstringSend this hostname to one of the unit’s named ports instead of its main port. For a service it is a port name as in self.ports.<name>, and naming it here allocates that port; in infra.hosts it is the container port for Compose services, or the port’s name for plain containers.inspector
infra.per_lanelist of stringCompose services run once per lane instead of shared, each in its own Compose project with ports from the lane’s block and its own named volumes, removed by devlanes lane clean. Needs Compose 2.24.4 or newer.["mailpit"]
infra.profileslist of stringCompose profiles to enable when resolving and starting the Compose services.["development"]
infra.project_namestring (templated)Name of the shared Compose project, fixed so docker compose -p <name> reaches the same containers outside devlanes. Defaults to the name Compose picks itself.acme
infra.setupmap of object or string (templated) or list of string (templated)Named commands that prepare each lane’s data, such as creating its database in a shared Postgres. They run once per lane after the containers are ready, all at the same time, and are recorded as done so later starts skip them; a failure stops the start unless the entry sets ignore_errors.{"database":"docker compose exec -T postgres createdb acme_${{ lane.name }}"}
infra.setup..containerstringRun this setup entry again whenever the named container has restarted since it last ran, for data that lives in the container and may be gone, such as a RabbitMQ vhost.rabbitmq
infra.setup..cwdstring (templated)Working directory, relative to the project file’s directory in the lane’s worktree; defaults to that directory.infra
infra.setup..envmap of scalar (templated)Variables for this command only.{"AWS_DEFAULT_REGION":"us-east-1"}
infra.setup..ignore_errorsboolean (templated)Count a failed or timed-out command as done instead of failing the start, for commands whose “already exists” error is fine. Defaults to false.true
infra.setup..runstring (templated) or list of string (templated)Command to run: a string runs through /bin/sh -c, a list runs the program directly. Write the command you mean, such as docker compose exec -T postgres ...; devlanes runs it as written. Required.pnpm dev
infra.setup..timeoutduration (templated)Longest the command may run. When it runs out, the command is stopped (SIGTERM, then SIGKILL after 10 seconds) and counts as failed, so a setup entry fails the start unless ignore_errors is set. No limit by default.15s
infra.teardownmap of object or string (templated) or list of string (templated)Named commands that remove a lane’s data when the lane is cleaned (devlanes lane clean), one after another; never on stop. A failure is reported and the cleanup goes on.{"database":"docker compose exec -T postgres dropdb --if-exists acme_${{ lane.name }}"}
infra.teardown..containerstringRun this setup entry again whenever the named container has restarted since it last ran, for data that lives in the container and may be gone, such as a RabbitMQ vhost.rabbitmq
infra.teardown..cwdstring (templated)Working directory, relative to the project file’s directory in the lane’s worktree; defaults to that directory.infra
infra.teardown..envmap of scalar (templated)Variables for this command only.{"AWS_DEFAULT_REGION":"us-east-1"}
infra.teardown..ignore_errorsboolean (templated)Count a failed or timed-out command as done instead of failing the start, for commands whose “already exists” error is fine. Defaults to false.true
infra.teardown..runstring (templated) or list of string (templated)Command to run: a string runs through /bin/sh -c, a list runs the program directly. Write the command you mean, such as docker compose exec -T postgres ...; devlanes runs it as written. Required.pnpm dev
infra.teardown..timeoutduration (templated)Longest the command may run. When it runs out, the command is stopped (SIGTERM, then SIGKILL after 10 seconds) and counts as failed, so a setup entry fails the start unless ignore_errors is set. No limit by default.15s
PathTypeDescriptionExample
lanesobjectWhich worktrees get a lane, how many ports each lane reserves, and values computed per lane (lane.<name>).{"hosts_file":"always","primary":"main","vars":{"db_name":"acme_${{ lane.name }}"}}
lanes.block_sizeinteger (templated)Number of ports reserved for each lane. By default devlanes counts the ports the config needs and adds half again, rounded up to ten. A fixed size smaller than what the config needs is an error.80
lanes.hosts_fileauto or never or alwaysWhether this project’s hostnames are also written to /etc/hosts, for programs that cannot resolve *.localhost by themselves (Wrangler’s workerd, musl or pure-Go binaries). Overrides the user’s hostnames.hosts_file (default auto), except that a user never always wins. Writing needs the helper devlanes setup installs, and only exact names are written, never wildcards. Values: auto: Write the exact hostnames only when the daemon’s startup check finds that the system resolver does not answer *.localhost with a loopback address. never: Never write hostnames to /etc/hosts. Set in the user config, it applies to every project whatever the project sets. always: Always write the exact hostnames, even when the system resolver handles *.localhost, for programs that send *.localhost to DNS themselves.auto
lanes.matrixlist of map of scalar (templated)Fixed values per lane instead of computed vars: entry N holds lane N’s values, read as lane.<name>. A lane whose index has no entry fails to resolve. Cannot be combined with vars.[{"domain":"acme.localhost","port":20000},{"domain":"acme1.localhost","port":20100}]
lanes.maxinteger (templated)Most lanes the project may hold at once; further worktrees stay unassigned, without ports. Lowering it keeps existing lanes. Unset, the number of lanes is limited only by free port blocks.6
lanes.new_worktreesregister or setup or startWhat devlanes does when a worktree gets its lane: only reserve it, also set it up, or also start it. Defaults to register. A worktree gets a lane the first time devlanes runs in it, not merely because it exists. Values: register: Only reserve the lane (its index, port block and domain); nothing runs until the lane is started. setup: Also run infrastructure setup and lane.create tasks, leaving services stopped. start: Also start the lane with the default profile.register
lanes.primarystringBranch whose worktree is lane 0, the main lane (lanes.main in expressions) and the only worktree that gets a lane by itself. Defaults to the remote’s default branch, else main or master. In CI (a variable such as CI or GITHUB_ACTIONS is set) the checkout takes lane 0 on any branch when lane 0 is free.main
lanes.removed_worktreesstop or cleanWhat happens to a lane whose worktree folder is deleted, for example by git worktree remove after a merge. Defaults to stop, which stops its processes and keeps its data, containers and ports until devlanes lane gc --yes or devlanes lane clean removes them. Set clean to have devlanes clean the lane by itself. Values: stop: Stop the lane’s processes and keep the lane: its data folder, containers, index and ports stay until devlanes lane gc --yes or devlanes lane clean removes them. clean: Clean the lane by itself: stop its processes, remove its own containers and data folder, and free its index and ports. An idle lane is cleaned once its folder has been gone for 5 seconds, so git worktree move keeps it. Its logs stay until log retention removes them. lane.clean tasks and infra.teardown need the folder to run in, so they are skipped; data they would drop from shared containers stays.stop
lanes.varsmap of scalar (templated)Values computed for each lane and read as ${{ lane.<name> }}, such as a database name, vhost, bucket or Redis index per lane. A var named domain replaces the default lane domain, and one named port fixes where the lane’s port block starts. ${{ domain('pages') }} gives a name a site of its own in each lane, such as pages2.localhost in lane 2. Cannot be combined with matrix.{"db_name":"acme_${{ lane.name }}"}
PathTypeDescriptionExample
profilesmap of list of string or objectNamed selections of services for devlanes start --profile <name>. A plain devlanes start brings back what the lane ran before; default is used when there is nothing to bring back, such as after devlanes stop. A profile is a list of names, globs and +tag selectors, or a mapping that can also exclude services, skip infra, add env and remap script names.{"default":["+api","+web"],"e2e":{"include":["*"],"run_scripts":{"dev":"start"}}}
profiles..envmap of scalar (templated)Variables for the services this profile starts, above their own env.{"NODE_ENV":"production"}
profiles..excludelist of stringServices removed from the include selection, by name, glob or +tag.["+desktop","sms-stub"]
profiles..includelist of stringServices this profile starts: names, globs such as crm-*, and +tag selectors. Globs skip services with autostart: false; names and tags include them.["+api","+web"]
profiles..infraboolean (templated)Set false to skip containers and infra.setup, for example to run only a frontend against another lane’s backend. Services that reference infra.* then fail to resolve. With several profiles active, infra is skipped only when all of them set false. Defaults to true.false
profiles..run_scriptsmap of stringSwap script names for services that use run_script: { dev: start } runs the start script wherever a service’s run_script names dev. The file stays the same, and each name is swapped once, not chained.{"dev":"start"}
PathTypeDescriptionExample
projectstringProject name, the same in every worktree of the repository. It builds each lane’s default domain (<project>.localhost for lane 0, <project><N>.localhost for lane N), names lanes in commands (acme@2), the containers devlanes creates and DEVLANES_LANE_PROJECT, and other projects reach this one as projects.<project>. Lowercase letters, digits, dots, underscores and dashes.acme
PathTypeDescriptionExample
servicesmap of objectLong-running processes of each lane, started together after infrastructure, setup and lane.start tasks. Each service gets its own port as PORT and, with hosts, HTTPS hostnames. A service that exits shows as failed and is not restarted.{"api":{"hosts":"api.${{ lane.domain }}","run_script":{"file":"apps/api/package.json","name":"dev"}}}
services..autostartboolean (templated)Set false for an on-demand service: a plain devlanes start without a default profile skips it unless the lane ran it before, and so do globs such as * in profiles. Naming it or its +tag, on the command line or in a profile, still starts it. An expression is evaluated for each lane and acts like the true or false it gives, such as ${{ lane.index == 0 }} to autostart only in the main lane. With replicas, the service starts all its copies when any copy’s expression gives true. Defaults to true.true
services..cwdstring (templated)Directory run starts in, relative to the project file’s directory in the lane’s worktree; defaults to that directory. Platform detection also looks here. Cannot be combined with run_script, which runs in the script file’s directory; devlanes config check refuses the pair, including a cwd inherited through extends.apps/api
services..envmap of scalar (templated)Environment variables for this service’s process. They override global_env and are overridden by the active profile’s env and --env; values may be text, numbers, booleans or ${{ ... }} expressions. Setting PORT replaces the port devlanes allocated; a literal number is the same in every lane, so lanes collide.{"API_URL":"${{ services.api.url }}"}
services..extendsstringTemplate under templates whose settings this service inherits; its own settings apply on top. Mappings (env, ready, stop, preset options, run_script) merge by key, lists (tags, hosts, sync) and single values replace. Setting run or run_script drops the inherited other one, and one ready probe (tcp, http, log, cmd) drops the inherited probe. Templates may extend other templates and are resolved after every project file and local override is merged.fastify-api
services..hiddenboolean (templated)Keep this service away from machines connected with devlanes remote connect. They never see, mirror or reach it, and do not get its sync folders. It still runs on this machine. Defaults to false.false
services..hostsstring (templated) or object or list of string (templated) or objectHostnames the devlanes HTTPS proxy routes to this service, so https://<host> reaches it. One entry or a list; an entry is a hostname or a mapping with host and optional port (a named port), fallback and host_header. A *. wildcard matches subdomains, and an exact host wins over it. The first non-wildcard host becomes public_url, non-wildcard hosts join the lane’s DEVLANES_ORIGINS, and having hosts makes the default ready a TCP check. Keep them under ${{ lane.domain }} so lanes do not collide.api.${{ lane.domain }}
services..hosts.fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for *.${{ lane.domain }} on a gateway that should answer every subdomain other services do not claim. Defaults to false.true
services..hosts.hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored.api.${{ lane.domain }}
services..hosts.host_headerpreserve or upstreamHost header the unit receives. Defaults to preserve, the hostname the browser asked for; upstream sends the local address, such as 127.0.0.1:<port>, for servers that reject or misroute requests for a Host that is not their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
services..hosts.portstringSend this hostname to one of the unit’s named ports instead of its main port. For a service it is a port name as in self.ports.<name>, and naming it here allocates that port; in infra.hosts it is the container port for Compose services, or the port’s name for plain containers.inspector
services..hosts[].fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for *.${{ lane.domain }} on a gateway that should answer every subdomain other services do not claim. Defaults to false.true
services..hosts[].hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored.api.${{ lane.domain }}
services..hosts[].host_headerpreserve or upstreamHost header the unit receives. Defaults to preserve, the hostname the browser asked for; upstream sends the local address, such as 127.0.0.1:<port>, for servers that reject or misroute requests for a Host that is not their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
services..hosts[].portstringSend this hostname to one of the unit’s named ports instead of its main port. For a service it is a port name as in self.ports.<name>, and naming it here allocates that port; in infra.hosts it is the container port for Compose services, or the port’s name for plain containers.inspector
services..keep_awakeboolean (templated)While this service runs, devlanes never throttles or freezes its lane, whatever lanes.throttle_after and lanes.freeze_after in the user config say. Set it on workers, queue consumers, schedulers and tunnels that do work without inbound requests, which a frozen lane would otherwise stall. One such service keeps the whole lane awake. An expression is evaluated for each lane, such as ${{ lane.index == 0 }} to keep only the main lane awake. Defaults to false.true
services..namestringLabel shown in the TUI and human output instead of the service key; any text, spaces allowed, with #N added per replica. Commands, profiles and ${{ services.<key> }} keep using the key.Admin API
services..nextobjectOptions for preset: next, which adds --port <self.port> --hostname 127.0.0.1 to the next dev command. Without the hostname flag Next listens on 0.0.0.0 instead of loopback.{"args":["--turbopack"]}
services..next.argslist of stringExtra arguments for next dev, added after the preset’s port and hostname flags, for example [--turbopack]. A local override or extends child that sets args replaces the inherited list. Default: none.["--turbopack"]
services..platformstringRuntime of the service: node, bun, deno, python or rust. It decides which variables devlanes adds, such as the devlanes CA (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE) and color settings. Detected from the run_script file or the working directory when omitted; set it only when detection is wrong. none skips detection and adds no platform variables.node
services..presetvite or next or wrangler or tauriFramework integration that adds the flags and variables a dev server needs to run in a lane, such as --port <port> --strictPort --host 127.0.0.1 for vite, so you do not pass them yourself. Requires run_script; with a hand-written run, drop the preset and pass its flags yourself. Its options go under a sibling key with the preset’s name, for example vite: { args: [--open] }.vite
services..ptyboolean (templated)Run the service in a pseudoterminal, so it sees a terminal as it would in your shell and you can type into it from the TUI. Set false to use pipes instead: stdin is empty and stdout and stderr are read separately. Defaults to true.true
services..readystarted or objectWhen the service counts as up: devlanes start waits for it, and after: services tasks run once every started service is ready. Write started (ready once the process runs) or a mapping with one of tcp, http, log or cmd, plus timeout (default 60s). Without it, a service with hosts waits for TCP on its port and one without hosts uses started. A check that times out marks the service failed, which also skips after: services tasks; the process keeps running and is not restarted. Values: started: Count the unit ready as soon as it runs, without probing anything. A dev server whose program crashed under a file watcher still counts as ready, so prefer a check when there is something to probe.started
services..ready.cmdstring (templated) or list of string (templated)Ready once this command exits 0. It runs on the host through /bin/sh (a list is quoted into one command line) with its output discarded, is retried until the timeout, and each attempt is cut off after 10 seconds. For a service it gets the service’s environment and working directory.curl -fsS ${{ self.url }}/healthz
services..ready.httpobjectReady once a GET request to path on the unit’s main port answers with status, or with a 2xx status when status is unset. Redirects are not followed. Best when the app has a health route that fails while it is broken. Each request sends User-Agent: devlanes-ready, so an app can leave the checks out of its request logs.{"path":"/healthz","status":200,"timeout":"60s"}
services..ready.http.hoststring (templated)Host header the check sends, for apps that answer only on their own hostname, such as ${{ self.hosts[0] }}. The request still goes to 127.0.0.1; only the header changes. Defaults to 127.0.0.1:<port>. Spaces and control characters are refused.${{ self.hosts[0] }}
services..ready.http.pathstring (templated)Path requested with GET, such as /healthz; defaults to /./healthz
services..ready.http.statusinteger (templated)Status code that counts as ready, between 100 and 599; only that exact code passes. When unset, any status from 200 to 299 counts, so a 404 or a redirect keeps waiting (redirects are not followed). Set it to accept another code, such as 302 for a route that redirects to a login.200
services..ready.http.timeoutduration (templated)How long the HTTP check may take to pass, replacing the check’s timeout; defaults to 60s. When it runs out, a service is marked failed but keeps running, and for an infra container the start fails.15s
services..ready.logstring (templated)Ready once a line of output matches this regular expression, with colors and other terminal escape codes removed; recent lines printed before the check began count too. Suits workers without a port: have them print a line once connected.Connected to Redis
services..ready.tcpboolean or stringReady once a port accepts a TCP connection on 127.0.0.1 or ::1, tried every 150 ms. true uses the unit’s main port; a port name such as inspector uses that named port. It only proves the socket is open, not that the app works.true
services..ready.timeoutduration (templated)How long the check may take to pass; defaults to 60s. When it runs out, a service is marked failed (its process keeps running and is not restarted) and after: services tasks are skipped; for an infra container the start fails. Raise it for services that compile before they listen.15s
services..replicasintegerNumber of copies of this service to run, each with its own port, for example several queue workers. They run as <name>.1 to <name>.N, ${{ replica.index }} counts from 1, and services.<name> in expressions means the first copy; use ${{ replica.index }} in hosts to give each copy its own hostname. A plain number of at least 1, the same in every lane, because each copy reserves ports in every lane’s block; ${{ ... }} expressions are not allowed. Defaults to 1.1
services..runstring (templated) or list of string (templated)Command that starts the service. A string runs with /bin/sh -c, so pipes, && and multi-line scripts work; a list is an argv run directly without a shell. It starts in the lane’s worktree root unless cwd is set. devlanes runs it exactly as written, so it cannot be combined with preset; prefer run_script when the command is a script in a project file. Exactly one of run and run_script is required after templates are applied.pnpm dev
services..run_scriptobjectScript to run from a project file, as { file, name }, such as { file: apps/api/package.json, name: dev }. devlanes picks the tool from the file name (package.json, Makefile, justfile, deno.json, pyproject.toml, Cargo.toml), runs the script in that file’s directory and checks that it exists before starting. Presets need it to place their flags, and a profile’s run_scripts can swap the script name. Exactly one of run and run_script is required after templates are applied.{"file":"apps/api/package.json","name":"dev"}
services..run_script.filestringFile that defines the script, relative to the project file’s directory, such as apps/api/package.json. The script runs in this file’s directory. Required unless an override or extends child keeps the inherited file.apps/api/package.json
services..run_script.namestringScript, target, recipe, task or binary name in that file. Required unless an override or extends child keeps the inherited name.dev
services..stopobjectHow devlanes stops the service: signal goes to its whole process tree, and after timeout every process still running is SIGKILLed. The service then shows as stopped and is not restarted. Defaults to SIGTERM and 10s.{"signal":"SIGINT","timeout":"15s"}
services..stop.signalstringSignal sent to the service’s whole process tree when devlanes stops or restarts it. One of SIGTERM (default), SIGINT, SIGHUP, SIGQUIT, SIGUSR1, SIGUSR2 or SIGKILL; use SIGINT for programs that only shut down cleanly on Ctrl-C.SIGTERM
services..stop.timeoutduration (templated)How long to wait after signal before devlanes SIGKILLs every process of the service that is still running. The service then shows as stopped; devlanes never restarts it. Defaults to 10s; raise it for programs that need time to flush or close connections.15s
services..synclist of objectFolders mirrored while the service runs, such as build output a browser loads unpacked from a fixed path. Each entry copies from to to (default ~/devlanes/sync/<project>/<lane>/<folder>) and follows changes; machines connected with devlanes remote connect get their own copy.[{"from":"apps/extension/.output/chrome-mv3","ignore":["*.map"]}]
services..sync[].fromstring (templated)Folder to copy, relative to the project file’s directory in the lane’s worktree.apps/extension/.output/chrome-mv3
services..sync[].ignorelist of stringGlob patterns of files left out of the copy and of later updates.["*.map"]
services..sync[].tostring (templated)Local destination; ~/ is your home and other relative paths start at the project file’s directory. Defaults to ~/devlanes/sync/<project>/<lane>/<folder>. Machines connected with devlanes remote connect ignore it and use their own path.~/devlanes/sync/acme/${{ lane.name }}/chrome-mv3
services..tagslist of stringLabels for selecting and grouping services. +tag selects every service with the tag in profiles, in commands such as devlanes start +api, and in the TUI filter. Tags also set the service’s color: the first tag with a color under the top-level tags wins, then the first tag naming a category such as api, web or worker, so put that tag first.["api","essential"]
services..tauriobjectOptions for preset: tauri, which lets several Tauri v2 dev apps run side by side, one per lane and replica. The values go into a config overlay at ${{ lane.data_path }}/tauri/<service>.<replica>.conf.json, passed with --config and in DEVLANES_TAURI_CONFIG, which Tauri merges over tauri.conf.json.{"before_dev_command":"","dev_url":"${{ services.desktop_web.url }}","identifier":"io.acme.app.lane${{ lane.index }}.client${{ replica.index }}"}
services..tauri.before_dev_commandstringReplaces build.beforeDevCommand, the command Tauri runs before the app. Set it to "" when the frontend runs as its own devlanes service; otherwise every replica starts its own frontend on the port from tauri.conf.json. Default: the command in tauri.conf.json.""
services..tauri.dev_urlstringFrontend address the app loads, written to build.devUrl. Usually another service’s url, such as ${{ services.desktop_web.url }}, so each lane’s app loads that lane’s frontend. Must start with http:// or https://; when unset, tauri.conf.json decides.${{ services.desktop_web.url }}
services..tauri.identifierstringBundle identifier for this lane and replica, which keys the app data, webview data and keychain entries. Required and must differ per lane and replica, for example com.acme.app.dev.lane${{ lane.index }}.client${{ replica.index }}; two instances with the same value are refused at start. Only letters, digits, - and . are allowed, with no empty part between dots.io.acme.app.lane${{ lane.index }}.client${{ replica.index }}
services..tauri.product_namestringReplaces the app’s productName from tauri.conf.json for this instance, for example to tell lanes apart. Default: the value in tauri.conf.json.Acme Dev
services..viteobjectOptions for preset: vite, which adds --port <self.port> --strictPort --host 127.0.0.1 to the dev server command. --strictPort makes Vite exit when the port is taken instead of moving to another port the proxy does not know about.{"args":["--open"]}
services..vite.argslist of stringExtra arguments for the Vite command, added after the preset’s port and host flags, for example [--mode, staging]. A local override or extends child that sets args replaces the inherited list. Default: none.["--open"]
services..wranglerobjectOptions for preset: wrangler, which gives each lane’s wrangler dev its own --port <self.port>, --inspector-port <self.ports.inspector> and dev registry. The inspector port is allocated automatically. args that pass either port flag are refused by devlanes config check, and a run_script script that does is refused at start.{"args":["--env-file",".dev.vars"],"isolate_registry":true,"vars":{"PUBLIC_BASE_URL":"${{ self.public_url }}"}}
services..wrangler.argslist of stringExtra arguments for wrangler dev, added after the port and --var flags, for example [--env-file, .dev.vars]. --port and --inspector-port (or --inspectorPort) are refused here, since the preset already sets them per lane. A local override or extends child that sets args replaces the inherited list.["--env-file",".dev.vars"]
services..wrangler.isolate_registrybooleanPoints WRANGLER_REGISTRY_PATH, CLOUDFLARE_DEV_REGISTRY_PATH and MINIFLARE_REGISTRY_PATH at wrangler-registry in the lane’s data folder (${{ lane.data_path }}), so service bindings resolve to workers of the same lane instead of same-named workers in another lane, also when two lanes run from one folder. Set false to drop these variables for this service, which then uses Wrangler’s shared default registry. Default: true.true
services..wrangler.varsmap of string or number or booleanWorker variables for this lane, each passed as --var KEY:VALUE so it overrides the same name in the Wrangler config’s vars. Values can be strings, numbers or booleans (sent as text) and may use expressions such as ${{ self.public_url }}; names must be non-empty and cannot contain :. Local overrides and extends children merge with inherited vars per name.{"PUBLIC_BASE_URL":"${{ self.public_url }}"}
PathTypeDescriptionExample
tagsmap of objectOptional color and description per tag. Services list their tags in their own tags, and tags group them: +api selects every service tagged api in profiles and in commands such as devlanes start +api. A unit’s color comes from the first of its tags with a color set here, else the first tag naming a category such as api, web, worker or db, else its preset or name. Tags not listed here work too.{"api":{"color":"cyan","description":"HTTP APIs"}}
tags..colorred or green or yellow or blue or magenta or cyan or white or gray or bright-red or bright-green or bright-yellow or bright-blue or bright-magenta or bright-cyanColor of services with this tag in the TUI and CLI output. When a service has several tags with a color, the first one in its tags list wins. Without one, devlanes infers a color from category words such as api (blue), web (green) or worker (magenta) in the service’s tags, preset or name. Values: red: Show the tag’s services in the terminal’s red color. green: Show the tag’s services in the terminal’s green color. yellow: Show the tag’s services in the terminal’s yellow color. blue: Show the tag’s services in the terminal’s blue color. magenta: Show the tag’s services in the terminal’s magenta color. cyan: Show the tag’s services in the terminal’s cyan color. white: Show the tag’s services in the terminal’s white color. gray: Show the tag’s services in the terminal’s gray color. bright-red: Show the tag’s services in the terminal’s bright-red color. bright-green: Show the tag’s services in the terminal’s bright-green color. bright-yellow: Show the tag’s services in the terminal’s bright-yellow color. bright-blue: Show the tag’s services in the terminal’s bright-blue color. bright-magenta: Show the tag’s services in the terminal’s bright-magenta color. bright-cyan: Show the tag’s services in the terminal’s bright-cyan color.red
tags..descriptionstringNote on what the tag groups, for people reading the config.HTTP APIs
PathTypeDescriptionExample
tasksmap of objectOne-shot commands such as installs, migrations and seeds, run by hand with devlanes run <name> or automatically at a lane event set with when. Tasks of the same event run in declaration order.{"migrate":{"run_script":{"file":"apps/api/package.json","name":"migrate"},"when":"lane.start"}}
tasks..afterservicesSet to services to run a lane.create or lane.start task after the started services are ready instead of before them, for work that goes through the running app, such as seeding through its API. Values: services: Run once every started service is ready; the task is skipped when one of them failed or the start timed out.services
tasks..confirmstring (templated)Question asked before the task runs, for destructive tasks. Scripts and agents without a terminal must pass --yes to run it.Reset ${{ lane.db_name }} and re-seed it?
tasks..cwdstring (templated)Working directory for run, relative to the project file’s directory in the lane’s worktree; defaults to that directory. Cannot be combined with run_script, which runs in the script file’s directory.apps/api
tasks..envmap of scalar (templated)Variables for this task only, above global_env.{"API_URL":"${{ services.api.url }}"}
tasks..interactiveboolean (templated)Give the task your terminal for input and output, for logins, prompts and TUIs. Interactive tasks only run by hand, so they cannot have when. Defaults to false.true
tasks..profileslist of stringRun this task’s when hook only while one of these profiles is active; default also matches a start without --profile. Runs by hand are not affected. Requires when.["default","backend"]
tasks..ptyboolean (templated)Run the task in a pseudoterminal instead of plain pipes, for tools that only print colors or progress to a terminal. Defaults to the value of interactive.true
tasks..runstring (templated) or list of string (templated)Command to run: a string runs through /bin/sh -c (pipes and multi-line scripts work), a list runs the program directly with those arguments. Use exactly one of run, run_script or steps.pnpm dev
tasks..run_scriptobjectScript to run from a project file, as { file, name }, such as { file: apps/api/package.json, name: migrate }. It runs with that file’s tool (the project’s package manager for package.json) in the file’s directory, and a missing script fails before anything starts. Prefer it to run for scripts that live in a file. Use exactly one of run, run_script or steps.{"file":"apps/api/package.json","name":"dev"}
tasks..run_script.filestringFile that defines the script, relative to the project file’s directory, such as apps/api/package.json. The script runs in this file’s directory. Required unless an override or extends child keeps the inherited file.apps/api/package.json
tasks..run_script.namestringScript, target, recipe, task or binary name in that file. Required unless an override or extends child keeps the inherited name.dev
tasks..stepslist of stringRun these tasks and infra.setup.<name> or infra.teardown.<name> entries in order, stopping at the first failure, for jobs such as resetting a database. Each step runs whatever its own when says, and setup entries run again even when already done. Use instead of run or run_script.["infra.teardown.database","infra.setup.database","migrate","seed"]
tasks..timeoutduration (templated)Longest the task may run. When it runs out, the task is stopped (SIGTERM, then SIGKILL after 10 seconds) and counts as failed; a failed task that runs before services fails the start. No limit by default.15s
tasks..whenlane.create or lane.start or lane.stop or lane.cleanLane event that runs this task automatically. Without it the task runs only by hand with devlanes run <name> or as a step of another task. Values: lane.create: Run on each start until it has succeeded once for the lane, then no longer automatically; for installs, code generation and a first seed. lane.start: Run on every start of the lane, before services unless after: services is set; for migrations and other steps that are safe to repeat. lane.stop: Run before the lane’s processes stop when the whole lane stops: devlanes stop without unit names, devlanes daemon stop and daemon restart. Skipped when a signal (logout, system shutdown) stops the daemon. lane.clean: Run when the lane is cleaned (devlanes lane clean), after its processes stop and before infra.teardown.lane.create
PathTypeDescriptionExample
templatesmap of objectNamed service settings that services reuse with extends: <template>. The service inherits every setting of the template and its own settings win: mappings (env, ready, stop, preset options, run_script) merge by key, lists (tags, hosts, sync) and single values replace. Setting run or run_script drops the inherited other one, and one ready probe (tcp, http, log, cmd) drops the inherited probe. Templates may extend other templates, are never started themselves, and are applied after every project file and local override is merged.{"fastify-api":{"platform":"node","ready":{"http":{"path":"/-/health"}},"tags":["api"]}}
templates..autostartboolean (templated)Set false for an on-demand service: a plain devlanes start without a default profile skips it unless the lane ran it before, and so do globs such as * in profiles. Naming it or its +tag, on the command line or in a profile, still starts it. An expression is evaluated for each lane and acts like the true or false it gives, such as ${{ lane.index == 0 }} to autostart only in the main lane. With replicas, the service starts all its copies when any copy’s expression gives true. Defaults to true.true
templates..cwdstring (templated)Directory run starts in, relative to the project file’s directory in the lane’s worktree; defaults to that directory. Platform detection also looks here. Cannot be combined with run_script, which runs in the script file’s directory; devlanes config check refuses the pair, including a cwd inherited through extends.apps/api
templates..envmap of scalar (templated)Environment variables for this service’s process. They override global_env and are overridden by the active profile’s env and --env; values may be text, numbers, booleans or ${{ ... }} expressions. Setting PORT replaces the port devlanes allocated; a literal number is the same in every lane, so lanes collide.{"API_URL":"${{ services.api.url }}"}
templates..extendsstringTemplate under templates whose settings this service inherits; its own settings apply on top. Mappings (env, ready, stop, preset options, run_script) merge by key, lists (tags, hosts, sync) and single values replace. Setting run or run_script drops the inherited other one, and one ready probe (tcp, http, log, cmd) drops the inherited probe. Templates may extend other templates and are resolved after every project file and local override is merged.fastify-api
templates..hiddenboolean (templated)Keep this service away from machines connected with devlanes remote connect. They never see, mirror or reach it, and do not get its sync folders. It still runs on this machine. Defaults to false.false
templates..hostsstring (templated) or object or list of string (templated) or objectHostnames the devlanes HTTPS proxy routes to this service, so https://<host> reaches it. One entry or a list; an entry is a hostname or a mapping with host and optional port (a named port), fallback and host_header. A *. wildcard matches subdomains, and an exact host wins over it. The first non-wildcard host becomes public_url, non-wildcard hosts join the lane’s DEVLANES_ORIGINS, and having hosts makes the default ready a TCP check. Keep them under ${{ lane.domain }} so lanes do not collide.api.${{ lane.domain }}
templates..hosts.fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for *.${{ lane.domain }} on a gateway that should answer every subdomain other services do not claim. Defaults to false.true
templates..hosts.hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored.api.${{ lane.domain }}
templates..hosts.host_headerpreserve or upstreamHost header the unit receives. Defaults to preserve, the hostname the browser asked for; upstream sends the local address, such as 127.0.0.1:<port>, for servers that reject or misroute requests for a Host that is not their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
templates..hosts.portstringSend this hostname to one of the unit’s named ports instead of its main port. For a service it is a port name as in self.ports.<name>, and naming it here allocates that port; in infra.hosts it is the container port for Compose services, or the port’s name for plain containers.inspector
templates..hosts[].fallbackboolean (templated)Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for *.${{ lane.domain }} on a gateway that should answer every subdomain other services do not claim. Defaults to false.true
templates..hosts[].hoststring (templated)Hostname to route, evaluated for each lane. A leading *. makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored.api.${{ lane.domain }}
templates..hosts[].host_headerpreserve or upstreamHost header the unit receives. Defaults to preserve, the hostname the browser asked for; upstream sends the local address, such as 127.0.0.1:<port>, for servers that reject or misroute requests for a Host that is not their own address. Values: preserve: Send the hostname the client requested, such as api.acme2.localhost. upstream: Send the local address and port the proxy connects to, such as 127.0.0.1:<port>.preserve
templates..hosts[].portstringSend this hostname to one of the unit’s named ports instead of its main port. For a service it is a port name as in self.ports.<name>, and naming it here allocates that port; in infra.hosts it is the container port for Compose services, or the port’s name for plain containers.inspector
templates..keep_awakeboolean (templated)While this service runs, devlanes never throttles or freezes its lane, whatever lanes.throttle_after and lanes.freeze_after in the user config say. Set it on workers, queue consumers, schedulers and tunnels that do work without inbound requests, which a frozen lane would otherwise stall. One such service keeps the whole lane awake. An expression is evaluated for each lane, such as ${{ lane.index == 0 }} to keep only the main lane awake. Defaults to false.true
templates..namestringLabel shown in the TUI and human output instead of the service key; any text, spaces allowed, with #N added per replica. Commands, profiles and ${{ services.<key> }} keep using the key.Admin API
templates..nextobjectOptions for preset: next, which adds --port <self.port> --hostname 127.0.0.1 to the next dev command. Without the hostname flag Next listens on 0.0.0.0 instead of loopback.{"args":["--turbopack"]}
templates..next.argslist of stringExtra arguments for next dev, added after the preset’s port and hostname flags, for example [--turbopack]. A local override or extends child that sets args replaces the inherited list. Default: none.["--turbopack"]
templates..platformstringRuntime of the service: node, bun, deno, python or rust. It decides which variables devlanes adds, such as the devlanes CA (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE) and color settings. Detected from the run_script file or the working directory when omitted; set it only when detection is wrong. none skips detection and adds no platform variables.node
templates..presetvite or next or wrangler or tauriFramework integration that adds the flags and variables a dev server needs to run in a lane, such as --port <port> --strictPort --host 127.0.0.1 for vite, so you do not pass them yourself. Requires run_script; with a hand-written run, drop the preset and pass its flags yourself. Its options go under a sibling key with the preset’s name, for example vite: { args: [--open] }.vite
templates..ptyboolean (templated)Run the service in a pseudoterminal, so it sees a terminal as it would in your shell and you can type into it from the TUI. Set false to use pipes instead: stdin is empty and stdout and stderr are read separately. Defaults to true.true
templates..readystarted or objectWhen the service counts as up: devlanes start waits for it, and after: services tasks run once every started service is ready. Write started (ready once the process runs) or a mapping with one of tcp, http, log or cmd, plus timeout (default 60s). Without it, a service with hosts waits for TCP on its port and one without hosts uses started. A check that times out marks the service failed, which also skips after: services tasks; the process keeps running and is not restarted. Values: started: Count the unit ready as soon as it runs, without probing anything. A dev server whose program crashed under a file watcher still counts as ready, so prefer a check when there is something to probe.started
templates..ready.cmdstring (templated) or list of string (templated)Ready once this command exits 0. It runs on the host through /bin/sh (a list is quoted into one command line) with its output discarded, is retried until the timeout, and each attempt is cut off after 10 seconds. For a service it gets the service’s environment and working directory.curl -fsS ${{ self.url }}/healthz
templates..ready.httpobjectReady once a GET request to path on the unit’s main port answers with status, or with a 2xx status when status is unset. Redirects are not followed. Best when the app has a health route that fails while it is broken. Each request sends User-Agent: devlanes-ready, so an app can leave the checks out of its request logs.{"path":"/healthz","status":200,"timeout":"60s"}
templates..ready.http.hoststring (templated)Host header the check sends, for apps that answer only on their own hostname, such as ${{ self.hosts[0] }}. The request still goes to 127.0.0.1; only the header changes. Defaults to 127.0.0.1:<port>. Spaces and control characters are refused.${{ self.hosts[0] }}
templates..ready.http.pathstring (templated)Path requested with GET, such as /healthz; defaults to /./healthz
templates..ready.http.statusinteger (templated)Status code that counts as ready, between 100 and 599; only that exact code passes. When unset, any status from 200 to 299 counts, so a 404 or a redirect keeps waiting (redirects are not followed). Set it to accept another code, such as 302 for a route that redirects to a login.200
templates..ready.http.timeoutduration (templated)How long the HTTP check may take to pass, replacing the check’s timeout; defaults to 60s. When it runs out, a service is marked failed but keeps running, and for an infra container the start fails.15s
templates..ready.logstring (templated)Ready once a line of output matches this regular expression, with colors and other terminal escape codes removed; recent lines printed before the check began count too. Suits workers without a port: have them print a line once connected.Connected to Redis
templates..ready.tcpboolean or stringReady once a port accepts a TCP connection on 127.0.0.1 or ::1, tried every 150 ms. true uses the unit’s main port; a port name such as inspector uses that named port. It only proves the socket is open, not that the app works.true
templates..ready.timeoutduration (templated)How long the check may take to pass; defaults to 60s. When it runs out, a service is marked failed (its process keeps running and is not restarted) and after: services tasks are skipped; for an infra container the start fails. Raise it for services that compile before they listen.15s
templates..replicasintegerNumber of copies of this service to run, each with its own port, for example several queue workers. They run as <name>.1 to <name>.N, ${{ replica.index }} counts from 1, and services.<name> in expressions means the first copy; use ${{ replica.index }} in hosts to give each copy its own hostname. A plain number of at least 1, the same in every lane, because each copy reserves ports in every lane’s block; ${{ ... }} expressions are not allowed. Defaults to 1.1
templates..runstring (templated) or list of string (templated)Command that starts the service. A string runs with /bin/sh -c, so pipes, && and multi-line scripts work; a list is an argv run directly without a shell. It starts in the lane’s worktree root unless cwd is set. devlanes runs it exactly as written, so it cannot be combined with preset; prefer run_script when the command is a script in a project file. Exactly one of run and run_script is required after templates are applied.pnpm dev
templates..run_scriptobjectScript to run from a project file, as { file, name }, such as { file: apps/api/package.json, name: dev }. devlanes picks the tool from the file name (package.json, Makefile, justfile, deno.json, pyproject.toml, Cargo.toml), runs the script in that file’s directory and checks that it exists before starting. Presets need it to place their flags, and a profile’s run_scripts can swap the script name. Exactly one of run and run_script is required after templates are applied.{"file":"apps/api/package.json","name":"dev"}
templates..run_script.filestringFile that defines the script, relative to the project file’s directory, such as apps/api/package.json. The script runs in this file’s directory. Required unless an override or extends child keeps the inherited file.apps/api/package.json
templates..run_script.namestringScript, target, recipe, task or binary name in that file. Required unless an override or extends child keeps the inherited name.dev
templates..stopobjectHow devlanes stops the service: signal goes to its whole process tree, and after timeout every process still running is SIGKILLed. The service then shows as stopped and is not restarted. Defaults to SIGTERM and 10s.{"signal":"SIGINT","timeout":"15s"}
templates..stop.signalstringSignal sent to the service’s whole process tree when devlanes stops or restarts it. One of SIGTERM (default), SIGINT, SIGHUP, SIGQUIT, SIGUSR1, SIGUSR2 or SIGKILL; use SIGINT for programs that only shut down cleanly on Ctrl-C.SIGTERM
templates..stop.timeoutduration (templated)How long to wait after signal before devlanes SIGKILLs every process of the service that is still running. The service then shows as stopped; devlanes never restarts it. Defaults to 10s; raise it for programs that need time to flush or close connections.15s
templates..synclist of objectFolders mirrored while the service runs, such as build output a browser loads unpacked from a fixed path. Each entry copies from to to (default ~/devlanes/sync/<project>/<lane>/<folder>) and follows changes; machines connected with devlanes remote connect get their own copy.[{"from":"apps/extension/.output/chrome-mv3","ignore":["*.map"]}]
templates..sync[].fromstring (templated)Folder to copy, relative to the project file’s directory in the lane’s worktree.apps/extension/.output/chrome-mv3
templates..sync[].ignorelist of stringGlob patterns of files left out of the copy and of later updates.["*.map"]
templates..sync[].tostring (templated)Local destination; ~/ is your home and other relative paths start at the project file’s directory. Defaults to ~/devlanes/sync/<project>/<lane>/<folder>. Machines connected with devlanes remote connect ignore it and use their own path.~/devlanes/sync/acme/${{ lane.name }}/chrome-mv3
templates..tagslist of stringLabels for selecting and grouping services. +tag selects every service with the tag in profiles, in commands such as devlanes start +api, and in the TUI filter. Tags also set the service’s color: the first tag with a color under the top-level tags wins, then the first tag naming a category such as api, web or worker, so put that tag first.["api","essential"]
templates..tauriobjectOptions for preset: tauri, which lets several Tauri v2 dev apps run side by side, one per lane and replica. The values go into a config overlay at ${{ lane.data_path }}/tauri/<service>.<replica>.conf.json, passed with --config and in DEVLANES_TAURI_CONFIG, which Tauri merges over tauri.conf.json.{"before_dev_command":"","dev_url":"${{ services.desktop_web.url }}","identifier":"io.acme.app.lane${{ lane.index }}.client${{ replica.index }}"}
templates..tauri.before_dev_commandstringReplaces build.beforeDevCommand, the command Tauri runs before the app. Set it to "" when the frontend runs as its own devlanes service; otherwise every replica starts its own frontend on the port from tauri.conf.json. Default: the command in tauri.conf.json.""
templates..tauri.dev_urlstringFrontend address the app loads, written to build.devUrl. Usually another service’s url, such as ${{ services.desktop_web.url }}, so each lane’s app loads that lane’s frontend. Must start with http:// or https://; when unset, tauri.conf.json decides.${{ services.desktop_web.url }}
templates..tauri.identifierstringBundle identifier for this lane and replica, which keys the app data, webview data and keychain entries. Required and must differ per lane and replica, for example com.acme.app.dev.lane${{ lane.index }}.client${{ replica.index }}; two instances with the same value are refused at start. Only letters, digits, - and . are allowed, with no empty part between dots.io.acme.app.lane${{ lane.index }}.client${{ replica.index }}
templates..tauri.product_namestringReplaces the app’s productName from tauri.conf.json for this instance, for example to tell lanes apart. Default: the value in tauri.conf.json.Acme Dev
templates..viteobjectOptions for preset: vite, which adds --port <self.port> --strictPort --host 127.0.0.1 to the dev server command. --strictPort makes Vite exit when the port is taken instead of moving to another port the proxy does not know about.{"args":["--open"]}
templates..vite.argslist of stringExtra arguments for the Vite command, added after the preset’s port and host flags, for example [--mode, staging]. A local override or extends child that sets args replaces the inherited list. Default: none.["--open"]
templates..wranglerobjectOptions for preset: wrangler, which gives each lane’s wrangler dev its own --port <self.port>, --inspector-port <self.ports.inspector> and dev registry. The inspector port is allocated automatically. args that pass either port flag are refused by devlanes config check, and a run_script script that does is refused at start.{"args":["--env-file",".dev.vars"],"isolate_registry":true,"vars":{"PUBLIC_BASE_URL":"${{ self.public_url }}"}}
templates..wrangler.argslist of stringExtra arguments for wrangler dev, added after the port and --var flags, for example [--env-file, .dev.vars]. --port and --inspector-port (or --inspectorPort) are refused here, since the preset already sets them per lane. A local override or extends child that sets args replaces the inherited list.["--env-file",".dev.vars"]
templates..wrangler.isolate_registrybooleanPoints WRANGLER_REGISTRY_PATH, CLOUDFLARE_DEV_REGISTRY_PATH and MINIFLARE_REGISTRY_PATH at wrangler-registry in the lane’s data folder (${{ lane.data_path }}), so service bindings resolve to workers of the same lane instead of same-named workers in another lane, also when two lanes run from one folder. Set false to drop these variables for this service, which then uses Wrangler’s shared default registry. Default: true.true
templates..wrangler.varsmap of string or number or booleanWorker variables for this lane, each passed as --var KEY:VALUE so it overrides the same name in the Wrangler config’s vars. Values can be strings, numbers or booleans (sent as text) and may use expressions such as ${{ self.public_url }}; names must be non-empty and cannot contain :. Local overrides and extends children merge with inherited vars per name.{"PUBLIC_BASE_URL":"${{ self.public_url }}"}

A project file’s local overlay holds the changes one developer wants on their machine only, such as a service turned off or a server they run themselves: devlanes.local.yaml for devlanes.yaml, devlanes.prod.local.yaml for devlanes.prod.yaml. It sits next to its project file and takes the same keys, every one optional, and the same modeline, # yaml-language-server: $schema=https://devlanes.run/v1/schema.json. Keep it out of Git: add it to .gitignore. A lane reads the overlay in its own worktree, else the primary worktree’s.

devlanes.local.yaml
services:
web:
autostart: false # a plain `devlanes start` skips it unless the lane ran it before or the default profile names it
infra:
external:
pgadmin:
address: 127.0.0.1:5050
hosts: "pgadmin.${{ lane.domain }}"

It is merged over its project file before templates are expanded. Mappings merge by key at every level (services, tasks, templates, profiles, the top-level tags, infra.containers, infra.external, infra.setup, infra.teardown, every env, lanes.vars), and a key the local file leaves out keeps its value. Lists and single values replace. run, run_script and a task’s steps drop each other, and so do the ready checks. A service only in the local file needs run or run_script. project and extends are refused there.

A project file builds on others with extends, one path or a list, relative to the file: devlanes.prod.yaml with extends: devlanes.yaml runs the same apps under another project. Each extended file is merged with its own local overlay, in list order, then the file itself and its overlay, by the same rules; later files win. An extended file may be partial and its project is ignored, so the extending file sets its own. Files that extend each other are an error. The ports and environment guide has the details under local overrides and extending another project file, and several projects in one directory says what two such projects share.

config.yaml in the devlanes home (~/.devlanes, or $DEVLANES_HOME), for settings of this machine. Schema: https://devlanes.run/v1/user-schema.json.

PathTypeDescriptionExample
daemonobjectThe daemon’s lifetime: whether it stops on its own once nothing runs and no TUI is open.{"exit_when_idle":false}
daemon.exit_when_idlebooleanStop the daemon about 15 seconds after the last TUI, or command that shows progress, closes, once nothing runs or starts, no box or client is connected or waiting to reconnect, and no pairing code is outstanding. A daemon that nothing ever watched this way, such as one only scripts or agents used, waits about 5 minutes instead. The next devlanes command starts it again. Containers keep running either way, but their hostnames answer only while the daemon runs. Defaults to true. Only a daemon a command started on demand exits. One installed with devlanes daemon install keeps running.true
PathTypeDescriptionExample
historyobjectHow long the database keeps records of finished service and task runs, and of remote boxes that no longer connect.{"remote_hosts_retention":"90d","retention":"7d"}
history.remote_hosts_retentiondurationHow long a remote box stays listed by devlanes remote ls after its last successful connection; defaults to 90d and must be longer than 0s. A record without a connection time is never deleted by age, and state.max_size can delete records sooner. A box connected with devlanes remote connect and not disconnected since is never deleted, so the daemon can connect it again after a restart.90d
history.retentiondurationHow long records of finished service and task runs (start and end times, exit status) are kept after the run ends; defaults to 7d and must be longer than 0s. A run that still has log index entries or a metrics summary in the database stays until those expire, and state.max_size can delete runs sooner.7d
PathTypeDescriptionExample
hostnamesobjectHow lane hostnames such as api.acme2.localhost resolve to this machine: the strategy (resolver), its tld, and whether names also go into /etc/hosts (hosts_file). The defaults need no setup for browsers that resolve *.localhost.{"hosts_file":"auto","resolver":"localhost"}
hostnames.base_domainstringReserved for the planned public resolver; any value is rejected for now.dev.example.invalid
hostnames.dns_providerobjectReserved for the planned public resolver’s DNS-01 certificates; any value is rejected for now.{"cloudflare":{"token":"${{ env.CF_API_TOKEN }}"}}
hostnames.dns_provider.cloudflareobjectReserved: Cloudflare credentials for DNS-01 certificates; not supported yet.{"token":"${{ env.CF_API_TOKEN }}"}
hostnames.dns_provider.cloudflare.tokenstring (templated)Reserved: Cloudflare API token allowed to edit the domain’s DNS records. Write an environment expression such as ${{ env.CF_API_TOKEN }}, not the token itself.${{ env.CF_API_TOKEN }}
hostnames.hosts_fileauto or never or alwaysWhether lane hostnames are also written to /etc/hosts, for programs that do not resolve *.localhost themselves (workerd, musl or pure-Go binaries); defaults to auto, which writes them only when a probe at daemon start shows the system resolver fails *.localhost. never here wins over every project; otherwise a project’s lanes.hosts_file replaces this value for that project. Writing needs the helper devlanes setup installs, only exact names are written (no wildcards), and resolver: hosts_file ignores this key. Values: auto: Write the exact hostnames only when the daemon’s startup check finds that the system resolver does not answer *.localhost with a loopback address. never: Never write hostnames to /etc/hosts. Set in the user config, it applies to every project whatever the project sets. always: Always write the exact hostnames, even when the system resolver handles *.localhost, for programs that send *.localhost to DNS themselves.auto
hostnames.resolverlocalhost or hosts_file or resolver or publicWhich domain lanes get and what makes their hostnames resolve; defaults to localhost. resolver also needs tld and a one-time devlanes setup; public is not supported yet. Values: localhost: Lane domains end in .localhost (acme.localhost, acme2.localhost), which browsers and many system resolvers send to loopback by themselves. hosts_file covers programs that do not. hosts_file: Keep .localhost names and always write exact hostnames to /etc/hosts, as hosts_file: always does, whatever hosts_file says; a project’s lanes.hosts_file still takes precedence. Needs the helper devlanes setup installs. resolver: Lane domains become <project>.<tld> and <project><N>.<tld>, and so do the names domain() returns. They are answered by a DNS server the daemon runs on 127.0.0.1:15353, so wildcard hostnames resolve too for programs that use the system resolver. Needs tld and a one-time devlanes setup. Projects that set lanes.vars.domain or lanes.matrix keep their own domains, and domain() gives them .localhost names. public: Reserved for public DNS names with DNS-01 certificates; not supported yet, so the config is rejected.localhost
hostnames.tldstringTop-level domain for the resolver strategy, such as test; required there. It must be test, internal, home.arpa or a domain under one of them (or under localhost), made of valid DNS labels, because any other name could belong to a real site; the file is rejected otherwise, whatever the strategy. A new devlanes CA may sign names under the tld set when it is created, whatever the strategy, so switching to another tld later needs a new CA.test
PathTypeDescriptionExample
lanesobjectWhere lane data folders (lane.data_path) live on this machine, and how long a lane may sit without activity before devlanes throttles or freezes it.{"data_dir":"~/.devlanes/data"}
lanes.data_dirstringFolder that holds every lane’s data folder, one subfolder per project; defaults to data in the devlanes home (~/.devlanes/data). Must be an absolute path or start with ~/. The daemon reads it when it starts. After a change and a daemon restart, lanes use new, empty folders under the new root; move the old folders there by hand to keep their data.~/.devlanes/data
lanes.freeze_afterdurationAfter this long without activity, devlanes freezes a lane: it suspends every process of the lane and pauses the lane’s own containers. Shared infra is never paused. Ports and process state are kept, so a frozen lane still counts as running. Activity is a request through the proxy, a new connection to one of the lane’s ports, the lane’s services using CPU, or a devlanes command on the lane; any of it resumes a frozen lane and restarts the timer. A request through the proxy resumes the lane at once and then goes through; a direct connection to a port is noticed within about half a second. When both are set above 0s, it must be longer than throttle_after (5m unless set). A running service with services.<name>.keep_awake: true keeps its whole lane awake. Defaults to 15m; 0s turns freezing off. The daemon reads it when it starts.15m
lanes.throttle_afterdurationAfter this long without activity, a lane’s processes run at background priority: lower CPU priority, and efficiency cores on machines that have them. They keep serving requests. Activity is a request through the proxy, a new connection to one of the lane’s ports, the lane’s services using CPU, or a devlanes command on the lane; any of it restores normal priority and restarts the timer. macOS and Windows only; Linux has no throttling, so lanes there stay at normal priority until freeze_after. When both are set above 0s, it must be shorter than freeze_after. A running service with services.<name>.keep_awake: true keeps its whole lane at normal priority. Defaults to 5m; 0s turns throttling off. The daemon reads it when it starts.5m
PathTypeDescriptionExample
logsobjectHow much service output devlanes keeps under ~/.devlanes/logs, by age and total size.{"retention":{"days":7,"max_size":"5GiB"}}
logs.retentionobjectAge and total-size limits for stored service output; a cleanup every 10 minutes deletes the oldest output past either one. Defaults to 7 days and 5GiB.{"days":7,"max_size":"5GiB"}
logs.retention.daysintegerDelete output older than this many days, along with its index entries in the database; defaults to 7 and must be at least 1. The newest log file of each stopped service is kept.7
logs.retention.max_sizestringTotal size of stored output across all projects and lanes; defaults to 5GiB and must be above zero. Over it, the oldest output goes first wherever it is, but each stopped service’s newest file and up to 1 MiB of a running service’s current file are kept.5GiB
PathTypeDescriptionExample
metricsobjectHow long CPU and memory history stays in the daemon’s database.{"retention":"7d"}
metrics.retentiondurationHow long per-minute CPU and memory samples, per-run summaries and startup memory peaks are kept; defaults to 7d and must be longer than 0s. Shorter keeps less history for graphs and for the memory a start is expected to need; state.max_size can delete samples sooner.7d
PathTypeDescriptionExample
portsobjectPort range that every lane of every project takes its block of ports from, so lanes never collide. Defaults to 20000-32767.{"range":"20000-32767"}
ports.rangestringPorts devlanes may hand out, written start-end (both included, start above 0); defaults to 20000-32767, below Linux’s usual ephemeral range so the OS does not pick them for outgoing connections. Each lane of each project gets its own block of consecutive ports from it, so a smaller range fits fewer lanes.20000-32767
PathTypeDescriptionExample
proxyobjectPorts of the daemon’s HTTP/HTTPS proxy, which serves every hosts entry, and whether other devices on the network may connect to it.{"http_port":80,"https_port":443,"lan":false,"via":true}
proxy.http_portintegerPort for plain HTTP; defaults to 80, falling back to 8080 like https_port, and must differ from it. While HTTPS runs it redirects known hostnames to HTTPS; without a CA it serves them over plain HTTP.80
proxy.https_portintegerPort the HTTPS proxy listens on; defaults to 443. When the port is taken or not allowed (low ports on Linux need extra permission), the daemon falls back to 8443 and service URLs carry the port.443
proxy.lanbooleanAccept connections from other devices on the network; defaults to false, which refuses every peer that is not this machine. Those devices must still resolve the hostnames to this machine and trust the CA.false
proxy.viabooleanAdd Via: 1.1 devlanes.run to every response the proxy forwards from a service (1.1 devlanes.run (remote) for lanes on a remote box), so a client can tell that devlanes sat in between. Defaults to true; set false if a client or test compares response headers exactly.true
PathTypeDescriptionExample
remoteobjectSettings for devlanes remote connect: boxes reached by a command, and how this machine serves clients when it is a box.{"allow_connections":false}
remote.allow_connectionsbooleanLet other machines connect to this one with devlanes remote connect. Defaults to true. When false, every client is turned away and devlanes daemon pair gives no codes. Connecting from this machine to a box still works. Clients already connected stay until they disconnect. An invalid value counts as false.true
remote.boxesmap of objectBoxes that devlanes remote connect <name> reaches by running a command instead of SSH, keyed by that name. Use it for a container, a WSL distribution or a codespace. Names not listed here are SSH destinations. Changes apply at the next remote connect.{"acme":{"command":["docker","exec","-i","acme","devlanes","tunnel","--stdio"]}}
remote.boxes..commandlist of stringThe program and its arguments as a list, one item per argument. No shell runs it. It must run devlanes tunnel --stdio in the box with its input and output passed through and no terminal, so use docker exec -i, not -it. It runs again for every reconnect and cannot ask for input, so sign in to its tool first.["docker","exec","-i","acme","devlanes","tunnel","--stdio"]
remote.require_pairingbooleanServe only clients paired with this machine. Defaults to true. A client’s first connection needs a one-time code from devlanes daemon pair, and devlanes daemon revoke removes a pairing. Set false to serve every client that reaches this machine over SSH or its command. An invalid value counts as true.true
remote.start_daemonbooleanStart the devlanes daemon here when a client connects and it is not running. Defaults to true. When false, a client keeps retrying until you run devlanes daemon start here. An invalid value counts as false.true
PathTypeDescriptionExample
startobjectWhat a plain devlanes start (no services, no --profile) brings up: the services the lane ran before, or its default selection.{"restore":true}
start.restorebooleanBring back the services the lane ran before, with the same profiles, so a reboot or daemon crash costs one devlanes start; defaults to true. A lane stopped as a whole with devlanes stop has nothing to bring back and starts its default profile. Set false to always start the default profile (or every autostart service). lane resume and daemon restart restore either way.true
PathTypeDescriptionExample
startupobjectHow many services may be starting at the same time across all lanes and projects, so a large start does not overload the machine. Defaults to one start per CPU core.{"max":4,"mode":"fixed"}
startup.initialintegerAdaptive starting window; defaults to max, so starts run at full speed until the machine is under pressure. Set it lower to ramp up from a few slots instead; never above max.2
startup.maxintegerMost services starting at once: the constant window in fixed mode and the ceiling in adaptive mode. Defaults to the number of CPU cores; all ignores it.8
startup.modeadaptive or fixed or alladaptive, fixed or all; default adaptive. A starting service holds its slot until its ready check (tcp, http, log or cmd) passes, it fails, or slot_timeout passes; one without a check (ready: started) holds it until it settles (settle_cpu). Values: adaptive: Begin each burst of starts at initial (by default max, full speed). Halve the slots after 3 s above 90% CPU or when the system reports memory pressure, start nothing while that pressure is critical, and add a slot every 2 s while system CPU stays below target_cpu, up to max. fixed: Allow max starts at a time, never more or fewer. A start also waits while free memory cannot fit what it needs. all: Start every service immediately, with no limit and no memory check.adaptive
startup.reserve_memorystringFree memory every new start must leave, on top of the memory the services already starting are expected to still need (each one’s past startup peak, or 512 MiB without history). Defaults to 10% of RAM, at most 2GiB; 0B turns it off. A start with nothing else starting never waits for it. all ignores it.2GiB
startup.settle_cpustringOnly for services without a ready check (ready: started, the default for a service without hosts): such a service gives its slot back once its process tree uses less than this share of one core for 2 s, meaning it has finished starting up. Services with a tcp, http, log or cmd check keep their slot until the check passes instead. Defaults to 10%.10%
startup.slot_timeoutdurationLongest a starting service holds its slot; after it, the next queued service may start even if this one is not ready, so services waiting on each other cannot block the queue. Defaults to 20s and must be above zero; the service itself keeps starting.15s
startup.target_cpustringSystem CPU use below which adaptive mode adds slots; defaults to 75%, and must be above 0% and at most 100%. Lower it to keep the machine responsive during large starts; other modes ignore it.75%
PathTypeDescriptionExample
stateobjectSize budget for the daemon’s database; past it, the oldest metrics and history are deleted before their retention ends.{"max_size":"512MiB"}
state.max_sizestringSize the database (state.db with its WAL and shared-memory files) should stay under; defaults to 512MiB and must be above zero. When it is larger, the cleanup deletes the oldest metric samples first, then the oldest history (finished runs, log index entries, remote boxes), even before their retention ends. Lanes, ports and running units are never deleted, so the file can stay above the budget.512MiB
PathTypeDescriptionExample
terminalstring or objectTerminal integration of the CLI, written as a mapping with progress (the tab’s progress bar during starts). A plain string, the terminal app name older versions used, is still accepted but ignored with a warning.{"progress":"auto"}
terminal.linksauto or always or neverWhether service URLs, config error locations and log or bundle paths are clickable links (the OSC 8 sequence); defaults to auto, which writes them in iTerm2, kitty, WezTerm, Ghostty, Windows Terminal, VTE terminals, Konsole, Alacritty and foot. Elsewhere the same text prints without a link. DEVLANES_TERM_LINKS=0 or 1 overrides this setting. Values: auto: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. always: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. never: Never use it.auto
terminal.marksauto or always or neverWhether long command output (logs of several services, failed starts) marks where each section starts, so the terminal’s jump-to-mark or jump-to-prompt keys move between them; defaults to auto: marks in iTerm2, and prompt marks (OSC 133) in WezTerm, Ghostty, kitty, foot and Windows Terminal. DEVLANES_TERM_MARKS=0 or 1 overrides this setting. Values: auto: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. always: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. never: Never use it.auto
terminal.notificationsauto or always or neverWhether a start that took a while finishing or failing, and a running service crashing, show a desktop notification while the terminal is not focused; defaults to auto. The TUI knows focus from the terminal; a command such as devlanes start does not, so with auto it notifies only in kitty, which itself skips a focused window. Terminals without notifications get the bell. At most one notification per 10 seconds. DEVLANES_TERM_NOTIFICATIONS=0 or 1 overrides this setting. Values: auto: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. always: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. never: Never use it.auto
terminal.progressauto or always or neverWhether starts show their progress in the terminal tab’s own progress bar (the OSC 9;4 sequence); defaults to auto. Only human output on a terminal gets it, and DEVLANES_TERM_PROGRESS=0 or 1 overrides this setting. Values: auto: Only in terminals known to draw it (Ghostty, WezTerm, iTerm2 3.6.6+, Windows Terminal, ConEmu, Ptyxis 48+, Konsole 26.04+); never inside tmux, screen, CI or a dumb terminal. always: In every terminal, for ones auto does not recognize such as kitty; a terminal without support may ignore it or show it as text or a notification. Inside tmux it is passed through, which needs allow-passthrough on. never: Never draw the progress bar.auto
terminal.titleauto or always or neverWhether the terminal tab’s title says what devlanes is doing (devlanes · acme@0 · starting 3/8 in the TUI, devlanes start acme@0 · 3/8 or devlanes logs -f api for long commands); defaults to auto. The previous title is saved on the terminal’s title stack and put back on exit, so auto sets it only where that stack exists: xterm, iTerm2, kitty, Ghostty and VTE terminals such as GNOME Terminal. Never inside tmux or screen, which manage titles themselves. DEVLANES_TERM_TITLE=0 or 1 overrides this setting. Values: auto: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. always: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. never: Never use it.auto
PathTypeDescriptionExample
tlsobjectWhich certificate authority signs the proxy’s HTTPS certificates (ca), and which stores devlanes setup installs it into (trust).{"ca":"devlanes","trust":["system","firefox"]}
tls.cadevlanes or mkcertWhich CA signs the certificate for each lane hostname; defaults to devlanes. Choose mkcert when your browsers already trust mkcert’s CA. Values: devlanes: devlanes creates its own CA in ~/.devlanes/ca on first use, limited to names under localhost, test, internal, home.arpa and hostnames.tld, so it cannot sign for real sites. Trust it once with devlanes setup. mkcert: Load mkcert’s existing root CA (from mkcert -CAROOT) instead, so browsers that already trust it need nothing new. HTTPS stays off when mkcert is not on PATH; the mkcert CA has no name limits, and devlanes setup --remove leaves it trusted.devlanes
tls.trustlist of system or firefox or chrome or windows or ios-simulator or android-emulatorStores devlanes setup installs the CA into and devlanes setup --remove removes it from; an empty list means [system]. On Linux, browsers do not read the system store, so add chrome and firefox there; inside WSL add windows for browsers on Windows. devlanes doctor shows each store’s state.["system","firefox"]
PathTypeDescriptionExample
tuiobjectWhether start commands open the terminal UI, how it looks, and what quitting it does with running lanes.{"open_on_start":false}
tui.glyphsauto or unicode or asciiCharacters the TUI draws states, markers and the spinner with; defaults to auto. Set ascii when your font shows boxes or misaligned symbols. Values: auto: Unicode, or ASCII when the locale (the first of LC_ALL, LC_CTYPE and LANG that is set) is not UTF-8. unicode: Always Unicode symbols, such as ● ✕ ▾ and a braille spinner. ascii: Plain ASCII only, for fonts or terminals that draw Unicode symbols badly.auto
tui.on_quitask or keep or stopWhat q or Ctrl+C does while lanes of the project on screen are running; defaults to ask. Ticking “remember” in the quit dialog writes the choice here. Values: ask: Show a dialog each time to choose between keeping the lanes running and stopping them. keep: Close the TUI at once; every lane keeps running in the daemon. stop: Stop the running lanes of the project on screen, then close the TUI. Lanes of other projects keep running.ask
tui.open_on_startbooleanOpen the TUI for start, restart, lane resume and lane new --start when they run at a terminal; defaults to true. Set false to have them print text instead, as --no-ui or DEVLANES_NO_UI=1 do.true
tui.themeauto or dark or light or ansiColor palette of the TUI; defaults to auto, which picks one from what the terminal reports. Choose dark or light when auto guesses wrong; NO_COLOR in the environment turns colors off whatever this says. Values: auto: Use the true-color dark or light palette when the terminal reports true color (COLORTERM) and its background (COLORFGBG); otherwise the terminal’s 16 colors. dark: Always use the true-color palette made for a dark background. light: Always use the true-color palette made for a light background. ansi: Use only the terminal’s 16 named colors, so the TUI follows the terminal’s color scheme.auto
PathTypeDescriptionExample
updatesobjectRelease checks: whether the daemon looks for newer devlanes releases (mode), and whether the TUI shows release highlights after an update (highlights).{"mode":"disable"}
updates.highlightsbooleanShow the release highlights dialog the first time the TUI opens after an update; defaults to true. Set false to skip the dialog. The new version still counts as seen, so turning this back on does not show the highlights of releases you already skipped.true
updates.modedisable or notify or autoWhat devlanes does about newer releases: notify (the default) checks every 4 hours and when the TUI opens, and shows the new version; auto also installs patch releases in the background where the install method allows it; disable never checks. DEVLANES_NO_UPDATE_CHECK=1 or a CI environment also turns checks off. Values: disable: Never look for releases; nothing is read from the network. notify: Read the latest version from the registry devlanes was installed from, and show it in the TUI and in command output when it is newer. Nothing is installed. auto: Like notify, and also install patch releases in the background where the install method allows it; they apply the next time you start devlanes at a terminal. Minor releases, which can break things before 1.0, still wait for you to update.disable