Skip to content

Migrating to devenv 2.0

This guide covers the breaking changes in devenv 2.0 and how to update your project.

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.

If you used processes.<name>.process-compose attributes, here’s how to translate them to native equivalents.

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"
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

Section titled “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";
};
}

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;
}

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:

Terminal window
# In your exec script:
while true; do systemd-notify WATCHDOG=1; sleep 10; done &
exec myapp
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
}

!!! tip “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.
Before
{
processes.server.process-compose = {
is_elevated = true;
};
}

For specific privilege needs, use Linux capabilities instead:

After
{
processes.server.linux.capabilities = [ "net_bind_service" ];
}

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-hooks

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:

Terminal window
# Before
pre-commit run --all-files
# After
prek run --all-files

devenv build now outputs JSON instead of plain store paths:

Terminal window
$ 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:

Terminal window
store_path=$(devenv build languages.rust.package)

Use jq to extract the value:

Terminal window
store_path=$(devenv build languages.rust.package | jq -r '.["languages.rust.package"]')

devenv container --copy <name> has been removed. Use the subcommand form instead:

Terminal window
$ devenv container copy <name>