Migrating to devenv 2.0
This guide covers the breaking changes in devenv 2.0 and how to update your project.
Native process manager is the default
devenv 2.0 replaces process-compose with a built-in Rust process manager. If your processes work without process-compose-specific configuration, no changes are needed — the native manager picks up processes.* definitions as before.
If you depend on process-compose features or want to keep using it during the transition:
devenv.nix
{
process.manager.implementation = "process-compose";
}
The native manager supports port allocation, readiness probes, socket activation, file watching, dependency ordering, watchdog heartbeats, and Linux capabilities. See the processes documentation for details.
If there’s something process-compose does that the native manager doesn’t yet cover, please let us know.
Migrating process-compose options
If you used processes.\<name\>.process-compose attributes, here’s how to translate them to native equivalents.
Dependencies
process-compose uses depends_on with conditions. The native manager uses after with lifecycle suffixes:
Before
{
processes.api.process-compose = {
depends_on.postgres.condition = "process_healthy";
depends_on.migrations.condition = "process_completed_successfully";
depends_on.cleanup.condition = "process_completed";
};
}
After
{
processes.api.after = [
"devenv:processes:postgres" # waits for readiness probe (= process_healthy)
"devenv:processes:migrations@succeeded" # waits for successful completion
"devenv:processes:cleanup@completed" # waits for exit regardless of success
];
}| process-compose condition | Native equivalent |
|---|---|
process_started | "devenv:processes:X@started" |
process_healthy | "devenv:processes:X@ready" or "devenv:processes:X" (default for processes; requires a ready probe on X) |
process_completed_successfully | "devenv:processes:X@succeeded" |
process_completed | "devenv:processes:X@completed" |
Restart policy
Before
{
processes.api.process-compose = {
availability = {
restart = "on_failure";
backoff_seconds = 2;
max_restarts = 5;
};
};
}
After
{
processes.api.restart = {
on = "on_failure"; # "never" | "always" | "on_failure"
max = 5;
window = null; # optional: sliding window in seconds for rate limiting
};
}
Note: backoff_seconds has no native equivalent. The native manager restarts immediately.
Environment variables and working directory
Before
{
processes.api.process-compose = {
environment = [ "NODE_ENV=production" "PORT=3000" ];
working_dir = "/app";
};
}
After
{
processes.api = {
env = {
NODE_ENV = "production";
PORT = "3000";
};
cwd = "/app";
};
}Readiness probes
The ready option works with both managers, so if you already use it, no changes are needed. If you used process-compose.readiness_probe directly:
Before
{
processes.api.process-compose = {
readiness_probe = {
exec.command = "curl -f http://localhost:8080/health";
period_seconds = 5;
failure_threshold = 3;
};
};
}
After
{
processes.api.ready = {
exec = "curl -f http://localhost:8080/health";
period = 5;
failure_threshold = 3;
};
}
The native manager also supports HTTP probes and sd_notify:
Native-only probe types
{
# HTTP probe
processes.api.ready.http.get = { port = 8080; path = "/health"; };
# sd_notify: process sends READY=1
processes.app.ready.notify = true;
}Liveness probes
process-compose supports liveness_probe separately from readiness_probe. The native manager has no liveness probe — use watchdog as an alternative for long-running health monitoring:
Before
{
processes.api.process-compose = {
liveness_probe = {
exec.command = "check-alive";
period_seconds = 30;
};
};
}
After
{
processes.api = {
ready.notify = true;
watchdog = {
usec = 30000000; # 30 seconds in microseconds
require_ready = true;
};
};
}
The watchdog requires the process to send periodic WATCHDOG=1 heartbeats via NOTIFY_SOCKET. If your process doesn’t support sd_notify, wrap it:
# In your exec script:
while true; do systemd-notify WATCHDOG=1; sleep 10; done &
exec myappShutdown signal
Before
{
processes.postgres.process-compose = {
shutdown.signal = 2; # SIGINT
};
}
Set the signal on the process itself. The setting applies to both the native manager and process-compose:
After
{
processes.postgres.shutdown.signal = 2; # SIGINT
}
New in version 2.2.3
processes.\<name\>.shutdown was added in devenv 2.2.3. Older 2.x versions send SIGTERM; wrap the process in a script that translates the signal if you need a different one.
Elevated processes
Before
{
processes.server.process-compose = {
is_elevated = true;
};
}
For specific privilege needs, use Linux capabilities instead:
After
{
processes.server.linux.capabilities = [ "net_bind_service" ];
}
On first launch, devenv shows the requested capabilities and authenticates with sudo. The service itself still runs as your user; only the listed Linux capabilities are retained. In non-interactive environments, authenticate with sudo -v before running devenv up. A privileged broker remains available for the manager’s lifetime, so detached processes and automatic restarts remain non-interactive.
git-hooks input is now optional
The git-hooks input is no longer included by default. If you use git-hooks.hooks in your devenv.nix, add the input explicitly:
devenv.yaml
inputs:
git-hooks:
url: github:cachix/git-hooks.nix
If you don’t use git-hooks, no changes are needed.
The pre-commit-hooks to git-hooks alias has also been removed. If you often switch between devenv v1.x and v2.x, add the pre-commit-hooks input as well to prevent lockfile changes when switching versions:
inputs:
pre-commit-hooks:
follows: git-hookspre-commit renamed to prek
The pre-commit command has been replaced by prek, a Rust rewrite. If you invoke pre-commit directly in scripts or shell commands, update them to use prek instead:
# Before
pre-commit run --all-files
# After
prek run --all-filesdevenv build returns JSON
devenv build now outputs JSON instead of plain store paths:
$ devenv build languages.rust.package
{
"languages.rust.package": "/nix/store/...-rust-1.83.0"
}
Update any scripts that parse the output. For example, if you previously did:
store_path=$(devenv build languages.rust.package)
Use jq to extract the value:
store_path=$(devenv build languages.rust.package | jq -r '.["languages.rust.package"]')devenv container subcommand cleanup
devenv container --copy \<name\> has been removed. Use the subcommand form instead:
$ devenv container copy <name>
Last updated Oct 11, 2026