Build a Custom systemd Target Without Breaking Boot

A systemd target sounds harmless, until someone isolates it on a live box and takes the login session with it. This walks you through building one safely: group two existing services under a named target, order it after them, enable it without touching those services, and verify the dependency graph before you ever isolate anything. The examples match systemd 255, installed here as 255.4-1ubuntu8.17.

Allow about 20 minutes if the services already exist. You need a systemd host, a shell, and root access for files under /etc/systemd/system. The inspection commands are ordinary user commands unless marked sudo.

Warning: do not test isolation on a remote production host without console access. systemctl isolate stops units outside the new transaction, and that can include your own connection.

1. Choose the target's job

A target is a named grouping and synchronisation point, not a process. It can pull in units through Wants= or Requires=, and it can impose ordering with After= or Before=. Those are two separate decisions: a target that lists a service in Wants= does not, by that fact alone, wait for the service to start.

This guide uses a local maintenance mode called app-maintenance.target. Replace the two example services with units that really exist on your host, and check them first:

$ systemctl list-unit-files 'example-api.service' 'example-worker.service'
$ systemctl cat example-api.service
$ systemctl cat example-worker.service

Checkpoint: stop here if either unit is absent, generated by another tool, or unsafe to start together. A target does not make an incomplete service deployment correct.

2. Write the target unit

Create the unit as root. The target file must not be empty; systemd treats an empty target as masked. The [Unit] section is the useful part, because target units have no separate [Target] section.

$ sudoedit /etc/systemd/system/app-maintenance.target

Paste this file, changing the service names if necessary:

[Unit]
Description=Application Maintenance Mode
Documentation=man:systemd.target(5)
Wants=example-api.service example-worker.service
After=example-api.service example-worker.service
AllowIsolate=yes

3. Check the file before loading it

Ask systemd to parse the unit and its dependencies without starting anything:

$ systemd-analyze verify /etc/systemd/system/app-maintenance.target

Success is no output and exit status 0. Warnings about missing example services are expected only if you deliberately used placeholders; replace them before continuing. A misspelled directive or an unsupported section is a configuration error, not something to shrug off because systemd kept parsing anyway.

Reload the manager once the file passes verification:

$ sudo systemctl daemon-reload
$ systemctl cat app-maintenance.target
[Unit]
Description=Application Maintenance Mode
Documentation=man:systemd.target(5)
Wants=example-api.service example-worker.service
After=example-api.service example-worker.service
AllowIsolate=yes

Checkpoint: the displayed unit must be the file you edited. If it is not, inspect the load path and any drop-ins with systemctl status app-maintenance.target and systemctl show -p FragmentPath app-maintenance.target.

4. Inspect ordering and activation

Before changing the running system, look at the transaction systemd would actually build:

$ systemctl list-dependencies --all app-maintenance.target
app-maintenance.target
* example-api.service
* example-worker.service
$ systemd-analyze dot app-maintenance.target | grep -E 'app-maintenance|example-(api|worker)'

The exact graph output varies, and the service names are host-specific. The check that matters is that both services show up as dependencies of the target. To see ordering properties directly:

$ systemctl show app-maintenance.target -p Wants -p Requires -p After -p Before
Wants=example-api.service example-worker.service
Requires=
After=example-api.service example-worker.service ...
Before=shutdown.target ...

Target units pick up default dependencies too: your configured Wants= and Requires= get complemented with After= ordering, and the target is conflicted with and ordered before shutdown.target. Do not treat that as a reason to drop your explicit ordering; it documents the synchronisation point and makes the unit's intent obvious to the next person.

5. Start it without isolating the host

Start the target as a normal unit first. This changes service state, so do it in a maintenance window if either service is production-facing:

$ sudo systemctl start app-maintenance.target
$ systemctl is-active app-maintenance.target
active
$ systemctl is-active example-api.service example-worker.service
active
active

If a service fails, inspect it before retrying:

$ systemctl status --no-pager example-api.service
$ journalctl -u example-api.service -b --no-pager

Starting the target is reversible: sudo systemctl stop app-maintenance.target undoes it. But stopping a target does not necessarily stop every unit it wanted. If you need a group whose members stop together, design that dependency relationship deliberately and test it on a non-critical host; do not assume Wants= gives you that lifecycle guarantee for free.

6. Enable it only when boot activation is wanted

A target's [Install] section is optional. Add one only if this target should be pulled in by a boot target or another enablement point:

[Install]
WantedBy=multi-user.target

With that section saved, enable it as root:

$ sudo systemctl enable app-maintenance.target
Created symlink /etc/systemd/system/multi-user.target.wants/app-maintenance.target -> /etc/systemd/system/app-maintenance.target
$ systemctl is-enabled app-maintenance.target
enabled

Enabling changes a symlink and affects future boots; it does not start the target now. sudo systemctl disable app-maintenance.target undoes it. If this target is meant for an occasional maintenance mode, do not enable it just because it worked fine when started manually.

7. Isolate only with a recovery path

Isolation replaces the active transaction with the target's transaction. It can stop your login session, networking, or other services. Before using it, confirm console access and keep a second administrative session open.

$ systemctl list-dependencies --all app-maintenance.target
$ sudo systemctl isolate app-maintenance.target

Afterwards, verify the result:

$ systemctl is-active app-maintenance.target
active
$ systemctl list-units --state=failed

To leave the mode, isolate a known-good target for your host, commonly multi-user.target or graphical.target:

$ systemctl get-default
graphical.target
$ sudo systemctl isolate graphical.target

Recovery: use the value systemctl get-default actually returned, not the example, and confirm it exists before isolating it. If you only started the custom target rather than isolating it, sudo systemctl stop app-maintenance.target is enough.

Done means