# docker-systemctl-replacement: an init and service manager for containers that were not built for one

> A Python script that can sit at /usr/bin/systemctl inside a container, start services from their own .service files, act as PID 1, and reap zombies. It exists so deployment automation written for machines works unchanged against a container.

**gdraheim/docker-systemctl-replacement** — docker systemctl replacement - allows to deploy to systemd-controlled containers without starting an actual systemd daemon (e.g.  centos7, ubuntu16)

- Repository: https://github.com/gdraheim/docker-systemctl-replacement
- Stars: 1,586 · Forks: 403
- Language: Python
- License: EUPL-1.2
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/gdraheim-docker-systemctl-replacement

## The problem is that systemd refuses to be PID 1 in a container

The README names the background plainly: there is no easy way to run a systemd daemon inside a docker container. It mentions the workarounds that have circulated with varying complexity and actual functionality, and notes that the systemd-nsspawn tool is supposed to help but was being used by rkt with CoreOS.

The consequence everyone hits is the same. Instead of running systemd, most people take the easy path and write a startup shell script that brings up service processes one by one, which means reading the application documentation or its systemd .service files to work out what the commands are.

That is the step this project removes. The script can be installed as /usr/bin/systemctl inside the container, and then systemctl start, systemctl enable and the rest keep working. Crucially, the output of systemctl show is provided as well, which is what lets deployment automation tools work rather than merely the interactive commands.

The target systems named are older distributions such as centos7 and ubuntu16, which tells you the era this was built for. The idea has not aged badly, because the constraint is structural rather than a bug: PID 1 in a container is your process, and init semantics are yours to implement or delegate.

## It is a service manager that reads the real .service files

The README calls this the Service Manager and points at a separate document, SERVICE-MANAGER.md, for detail.

The mechanism is not a rewrite of how to start Apache or MariaDB. It parses the *.service files that the standard application packages install in the container, whether from rpm or deb packaging, and honours the ExecStart and ExecStop settings those unit descriptors define. So the source of truth is the same file a real machine would use, which means an application package behaves the way its packager intended rather than the way a shell script author guessed.

The claim is that installing systemctl3.py as /usr/bin/systemctl provides enough functionality that deployment scripts written for virtual machines continue to work unchanged for start and stop, enable and disable, and mask and unmask operations inside a container.

The README shows what that looks like in practice. A container reports running units:

```
httpd.service     loaded active running   The Apache HTTP Server
mariadb.service   loaded active running   MariaDB database server
```

and a process tree shows the replacement script at PID 1 with the started services beneath it:

```
systemctl,1 /usr/bin/systemctl
  |-httpd,7 -DFOREGROUND
  |   |-httpd,9 -DFOREGROUND
  |   |-httpd,10 -DFOREGROUND
  `-mysqld_safe,44 /usr/bin/mysqld_safe --basedir=/usr
```

The README is careful to call the coverage sufficient rather than complete. Unit types beyond what deployment scripts touch, and systemd features with no practical equivalent in a container, are not the target.

## Ansible is the use case the README keeps returning to

The most concrete adoption path in the README is Ansible.

Since Ansible 2.0 it has been able to connect to docker containers directly without an ssh-daemon inside the container, using an inventory entry like this:

```ini
[frontend]
my_frontend_1 ansible_connection=docker
```

Based on that connection type you can enable the replacement to intercept subsequent calls to service steps, so playbooks written for real virtual machines can be run against containers for testing.

The README also gives the four steps a playbook needs, and they are more informative than they look:

```yaml
- copy: src="files/docker/systemctl3.py" dest="/usr/bin/systemctl"
- package: name="python"
- file: name="/run/systemd/system/" state="directory"
- service: name="dbus.service" state="stopped"
```

The second step exists because in newer CentOS and Ubuntu images you need to check for python first, which the README flags explicitly. The third and fourth steps mirror the configuration shown in the README example.

That dbus step is the clearest illustration of the project's boundary. You are running a stand-in for systemd in an environment that still has systemd expecting pieces, and each such collision has to be resolved by hand.

## Running as PID 1 solves the zombie problem containers create

The second function is an init daemon, documented in INIT-DAEMON.md.

The problem it addresses is specific. The command listed as CMD becomes PID 1 of the container, taking the place of /sbin/init. When docker stop runs, a SIGTERM goes to PID 1 and to nothing else. If your CMD is the application itself, that usually suffices because it cleans up its own subprocesses. In many other cases it does not, and you are left with zombie processes.

Zombies appear when a master process never waits for its children, or when children were explicitly disowned to daemonise themselves. The README explains the container specific consequence: those zombie PIDs live for as long as the container runs, and they also fill the process table of the docker host, because the host's own init daemon will not reap another container's children.

The replacement implements the zombie reaping that a standard unix init daemon provides. It can also run as the container entrypoint, in which case it automatically starts every service enabled for multi-user.target, reaps zombies in the background, and on SIGTERM brings down all the configured services before exiting by executing the corresponding stop commands.

That shutdown behaviour is the reason to run it as PID 1 rather than just installing it as the systemctl binary. A container that exits without stopping MariaDB cleanly is a problem you will debug much later than the deployment step that caused it.

## Packaging, entry points and an unusual version scheme

The package on PyPI is docker_systemctl_replacement, at version 1.7.1311, authored by Guido U. Draheim, and the licence is EUPL-1.2 with the licence text in EUPL-LICENSE.md. The repository has no GitHub releases.

The version number is not a mistake. The Makefile has a version target that computes the version from dates, using an ORIGYEAR of 2016, a BASEYEAR of 2025, and a FOR variable defaulting to today, with a comment showing how to run make version FOR=yesterday or with a day offset. It rewrites the version field in pyproject.toml and the __version__ markers across a list of files that includes the scripts, the tests and the dockerfiles, then prints the resulting version and a suggested git commit message.

So the trailing digits encode a build date rather than a release sequence, which is why a patch level can move to a large number without any API change. Reading this as semver would be wrong.

Entry points are declared twice, old style and new style. The old style exposes systemctl.py and systemctl3.py, plus journalctl.py and journalctl3.py, mapped to modules in a package directory named systemctl2. The new style exposes systemctl3 and journalctl3 with the same targets. The package data mapping shows systemctl2 pointing at src and systemctl3 at its own directory, so the legacy layout is still shipped.

The classifier list includes Typing :: Typed, and the repository has TYPEHINTS.md. CI runs three workflows visible in the badges, a lint check, a type check and unit tests, with a coverage badge reporting 94 percent across 517 tests.

## What it does not do, and who should not use it

It is not systemd. The README frames it as a replacement that provides enough functionality for deployment scripts, rather than as systemd itself.

It is not a production container runtime pattern. Running an init replacement inside a container adds a layer that most production setups avoid entirely, in favour of running one process as PID 1 or using a supervisor with explicit restart policies. The README's framing is about testing deployments, and that framing should survive into your decision.

It does not remove the need for python in the image, which is the most common first failure, and the Makefile's own test targets reference an ubuntu:18.04 image, which dates the default test environment.

The documentation is unusually well organised for a small project, and that is part of the adoption case. Separate documents cover ARCHITECTURE.md, BUGS.md, DEFAULTPYTHON.md, DEVGUIDE.md, EXTRA-CONFIGS.md, HISTORY.md, INIT-DAEMON.md, RELEASENOTES.md, RESTART-POLICY.md, SERVICE-MANAGER.md, SYSTEMD_CONF.md, TESTSUITE.md, TYPEHINTS.md and USERMODE.md. A project that documents its own limits in that many files is one you can evaluate.

The repository is not archived and the last push was on 2026-08-02.

## Conclusion

Adopt docker-systemctl-replacement when your deployment automation is written against systemctl and your test environment is a container, since it removes the need to rewrite every service step into a startup shell script. Do not adopt it in production containers, where a supervisor with real process management is the better answer, and do not expect it to replace a full init, since it implements the subset of systemd that deployment tools call. Before running it, read SERVICE-MANAGER.md for the supported unit subset, create /run/systemd/system/ as the Ansible example does, and confirm your base image has python installed, which newer CentOS and Ubuntu images do not assume.

## FAQ

### What does docker-systemctl-replacement actually replace?

It can be installed as /usr/bin/systemctl inside a container and executes systemctl commands without a systemd daemon, by parsing the .service files that packages install and honouring their ExecStart and ExecStop settings.

### How does it work with Ansible?

Ansible can connect to containers directly using ansible_connection=docker, and the replacement intercepts the resulting service steps, so playbooks written for virtual machines can be tested against containers. The README notes you must check for python first in newer CentOS and Ubuntu images.

### Can I run it as the container entrypoint?

Yes. As PID 1 it starts all services enabled for multi-user.target, reaps zombie processes, and on SIGTERM from docker stop it brings down the configured services before exiting.

### Why is the version number 1.7.1311?

The Makefile computes versions from dates, with an origin year of 2016 and a base year of 2025, rewriting version fields across pyproject.toml, the scripts, the tests and the dockerfiles. The trailing digits encode a build date rather than a release sequence.

### What licence is docker-systemctl-replacement under?

EUPL-1.2. The licence text is in EUPL-LICENSE.md at the repository root and is declared in the project metadata.

## Sources

- [gdraheim/docker-systemctl-replacement on GitHub](https://github.com/gdraheim/docker-systemctl-replacement)
- [Issues](https://github.com/gdraheim/docker-systemctl-replacement/issues)
- [License: EUPL-1.2](https://github.com/gdraheim/docker-systemctl-replacement/blob/master/LICENSE)
- [README](https://github.com/gdraheim/docker-systemctl-replacement/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gdraheim-docker-systemctl-replacement
