immortal: a small supervisor with a control socket per service
⭕ A *nix cross-platform (OS agnostic) supervisor
At a glance
- What is it?
- immortal is a Go process supervisor for Unix-like systems that keeps a service running, rotates its logs and exposes a Unix socket per service so you can query status and send up or down signals without signals or pid hunting. It is BSD-3-Clause licensed, written in a single package with a small dependency list, and it is not a systemd replacement.
- Who is it for?
- Adopt immortal if you supervise a handful of long running Unix services outside systemd, especially when you want log rotation and a queryable control socket per service. Do not adopt it as an init system replacement, since it supervises processes rather than managing the boot, users or dependencies between units.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A supervisor, not an init system
immortal describes itself as a *nix cross-platform (OS agnostic) supervisor, and the distinction from an init system is the first thing to get right. It supervises processes. It does not manage the boot sequence, it does not set up users and groups for units, and it does not model ordering dependencies between them the way a service manager does.
What it does do is watch one process, keep it alive, rotate its output logs, and give you a way to talk to it over a Unix socket. The service definition is a YAML file, one per service, and the process it runs is described with a cmd, a working directory, log settings and optional requirements.
The audience is people running long lived services on Unix-like systems who want something lighter than a full service manager, and who want control over the process rather than just the ability to start and stop it. The homepage is immortal.run, and there is GoDoc documentation, a contributing guide and a contributions welcome badge.
The repository layout shows a single Go package at the root rather than an internal tree. There are files for the daemon, the control logic, the fork and exec, the process supervision, the logger, the signals handling, the socket layer, the config file parser and the flags, plus per-platform files for things that differ by operating system: kqueue implementations for BSD and Linux, scandir for Linux, openmode for Darwin, and watchpid variants per architecture. The cmd directory holds the three binaries. The a_test.go file at the root is the cross-platform test entry point, and there is a rust directory whose purpose the README does not explain.
The dependency list in go.mod is short and entirely first party except for one package: the immortal fork of logrotate, a multiwriter, a natural case sorter, an xtime helper, a YAML parser and violetear, a router used for the HTTP status endpoint.
Configuration is one YAML file per service
The configuration example in the documentation is the whole story. The file at /usr/local/etc/immortal/www.yml looks like this:
# pkg install go-www
cmd: www
cwd: /usr/ports
log:
file: /var/log/www.log
age: 10 # seconds
num: 7 # int
size: 1 # MegaBytes
wait: 1
require:
- foo
- bar
Four things are worth noticing. The cmd is the command to run and cwd is its working directory. The log section controls rotation with three knobs: age in seconds, num as a count of files to keep, and size in megabytes. And the require list names other services.
The require semantics are the interesting bit. If foo and bar are not running, the service www will not be started. The names refer to other services defined in the same directory, resolved by filename, so www.yml sits next to foo.yml and bar.yml. That is a dependency mechanism, and it is worth being precise about what it is: it is a start-time gate, not a supervision relationship. A service that requires another is not restarted when that other dies, it simply will not come up if the other is not already running.
The documentation also tells you to skip the age, num and size options to avoid log rotation completely, and the go.mod shows why that is a real choice rather than a default: log rotation is implemented through a separate module, the immortal fork of logrotate, so the size and age checks are the supervisor's own, separate from a system logrotate policy.
The example directory in the repository shows the range of what the parser accepts, with files for retries, running a command, requirements, environment variables, stderr only and a bad run, so the config surface is broader than the single example suggests.
One control socket per service
The control mechanism is what distinguishes immortal from a plain restart loop, and it is a Unix domain socket per supervised service.
When services are launched through immortaldir, the runtime directory looks like this:
/var/run/immortal
|--foo
| |-lock
| `-immortal.sock
|--bar
| |-lock
| `-immortal.sock
`--www
|-lock
`-immortal.sock
Each service gets a directory containing a lock file and a socket named immortal.sock. The lock file is how the supervisor avoids two instances fighting over the same service.
For a non-root user who launches a service directly, the same structure appears under the home directory, in ~/.immortal, with one directory per process. The -name argument takes a non-path name such as myservice and is used instead of the pid in the directory structure, so immortal -ctl myservice sleep 1000 produces ~/.immortal/myservice containing the lock and the socket. The -ctl argument takes precedence over -name, which is how the control client addresses a specific instance.
The socket speaks HTTP, and the documentation shows it being driven with curl and jq. To read status:
curl --unix-socket immortal.sock http:/status -s | jq
To take the service down:
curl --unix-socket immortal.sock http://im/signal/d -s | jq
And to bring it up:
curl --unix-socket immortal.sock http://im/signal/u -s | jq
Note the single slash in the status URL, which the documentation flags with a link, since curl normalizes a path with a double slash and would then try to reach an actual host. The HTTP interface is why violetear, a router, is in the dependency list.
The user-facing tool is immortalctl, which prints the current status and allows you to manage the services. The debug recipe in the documentation shows how to watch the process tree:
pgrep -fl "immortal -ctl" | awk '{print $1}' | xargs watch -n .1 pstree -p
That is a real operational difference from a cron-based watchdog. The supervisor is reachable by name, and it is reachable without a pid file path you have to guess.
Building it, and the cgo catch on user lookups
The documentation gives precompiled Linux binaries as deb and rpm packages on Packagecloud, and ports for FreeBSD and macOS through pkg and homebrew. For other platforms it asks for help making a port or package.
Building from source needs a Go environment, and the README states that go >= 1.12 is required, while go.mod declares go 1.22. The documented sequence uses the classic GOPATH layout:
$ export GOPATH=$HOME/go
$ mkdir -p $HOME/go/src/github.com/immortal
$ git clone [email protected]:immortal/immortal.git $HOME/go/src/github.com/immortal/immortal
$ cd $HOME/go/src/github.com/immortal/immortal
$ make
The Makefile builds three binaries, immortal, immortalctl and immortaldir, and stamps the version in from git describe --tags --always. There are also make install and make uninstall targets, and a build-linux target that cross compiles for a long list of architectures including 386, amd64, arm, arm64, ppc64, ppc64le, mips, mipsle, mips64 and mips64le, plus a build-fbsd target.
The cgo warning is the sharpest practical detail in the whole document. If services need to run on behalf of another system user such as www, nobody or www-data rather than root, immortal should be compiled from source for the desired target and architecture, otherwise this error may be returned:
Error looking up user: "www". user: Lookup requires cgo
That is because resolving a user by name needs the system user database, which needs cgo. A prebuilt binary that was built without it will fail at exactly the moment you try to start a service under a different user, which is a common production configuration and a confusing failure if you did not read this paragraph.
The Dockerfile shows how the project itself builds and packages. It runs go test -race -v in a builder stage, builds the Linux binaries, then uses Ruby with fpm to produce deb and rpm packages, with a VERSION argument passed in as a build argument and copied into the environment.
What immortal is not, and where a service manager wins
The honest comparison is with a service manager, and the difference is mostly about scope rather than quality.
A service manager gives you dependency ordering that is modeled properly, with units that can require and be required by others, and it restarts failed units with backoff. It handles per-service users, groups, resource limits, sandboxing and standard output capture wired into the journal. It gives you a declarative unit file, timers, socket activation and a well understood operational contract across the whole operating system.
immortal gives you a much smaller thing: a process that is restarted, a log that is rotated, and a socket that answers status and up or down signals. The require list is a start gate, not a supervision relationship. The restart policy is not described in the configuration example at all, which is a documentation gap rather than a design statement.
Where it wins is footprint and portability. It is a single Go binary with a handful of dependencies, it compiles to every architecture in the Linux list above, and the same tool works on FreeBSD and macOS. For a single service on a container or a small appliance, installing a full service manager to supervise one process is a lot of ceremony.
The other comparison worth making is with running the process under a supervisor written in the language of the service itself, which is what a web application often does. immortal is language agnostic, which is the point, and the trade is that it knows nothing about your application's readiness, so its wait value is a fixed delay rather than a health check.
One operational limit shows up in the log design. Because rotation is done by the supervisor with age, num and size, running immortal on a host that also has a system logrotate policy for the same file means two rotation systems can fight over it. Pick one.
Maintenance, licence and the release gap
The repository is not archived and the last push was on 2026-10-22, so work on the project is recent. The release history is where the story gets more complicated. The listed tags are v0.24.7 from 2026-07-10, v0.24.6 from 2024-07-21 and 0.24.5 from 2024-02-01.
So the version number has barely moved across two years, while the last push is weeks old. The Makefile derives the version from git describe --tags --always, so a build from the current branch reports something different from the latest tag. That combination, a low version number with recent commits, suggests an actively worked on project where releases are infrequent, or a project where a large amount of work lands on master between tags.
The licence is BSD-3-Clause, with the LICENSE file at the repository root. BSD is permissive, with a no-warranty clause, and it places no obligation on a service you supervise with it. There is nothing in the licence that reaches the software immortal runs, which is the arrangement you want from a supervisor.
For an evaluator, the two things to check are concrete. First, whether the version you install from a package repository matches the commit you would get by building from source, since the release cadence is slow. Second, whether the cgo requirement is satisfied in the artifact you plan to deploy, because the prebuilt packages and a locally compiled binary differ on that point in a way that only shows up when a service runs as a non-root user.
The CI setup is visible too, with CircleCI, Travis and coveralls and codecov badges in the README, a Dockerfile that runs the race-enabled test suite during the build, and a CHANGELOG.md at the root, so there is a regression suite behind the tool even if the release tags are sparse.
Editorial conclusion
Adopt immortal if you supervise a handful of long running Unix services outside systemd, especially when you want log rotation and a queryable control socket per service. Do not adopt it as an init system replacement, since it supervises processes rather than managing the boot, users or dependencies between units. Verify first that the binary you install was compiled with cgo if your service must run as another user, that the prebuilt Linux packages match your architecture, and that the log rotation settings match the logrotate policy already on the host.
Frequently asked questions
What is immortal and what does it do?
immortal is a cross-platform Unix supervisor written in Go. It keeps a service process running, rotates its output logs, and exposes a per-service Unix socket for status and up or down signals.
How do I configure an immortal service?
With one YAML file per service in the configuration directory, for example /usr/local/etc/immortal/www.yml. The example sets cmd, cwd, a log section with file, age, num and size, a wait value, and a require list naming other services in the same directory.
Why do I get Lookup requires cgo when running a service as another user?
Resolving a user name needs the system user database, which needs cgo. The README says immortal should be compiled from source for the desired target and architecture if services must run as another user such as www, nobody or www-data, otherwise the error Error looking up user appears.
How do I check the status of an immortal service?
Use the immortalctl tool, which prints the current status and lets you manage services. The status can also be fetched over the service socket with `curl --unix-socket immortal.sock http:/status -s | jq`, using a single slash as the documentation notes.
Can immortal replace systemd?
No. It supervises individual processes with log rotation and a control socket, but it does not model boot ordering, per-service users, resource limits or timer units the way a service manager does. The require list in a service file is a start gate, not a supervision relationship.
What license is immortal released under?
BSD-3-Clause, with the text in the LICENSE file at the repository root. The licence places no obligation on the software that immortal supervises.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/immortal-immortal)Was this useful?