SYSTEMD-RUN(1)

๐Ÿ’– systemd-run โ€” the systemd-run manual, but make it make sense โœจ

okay bestie, so systemd-run lives in the systemd family (that's the manual name, it's giving SYSTEMD-RUN(1) energy ๐Ÿ’…) and it's literally the "run my program but make it a unit" command. Let's go ๐Ÿฆ‹


๐ŸŒˆ SYNOPSIS ๐ŸŒˆ

systemd-run [OPTIONS...] COMMAND [ARGS...]

systemd-run [OPTIONS...] [PATH OPTIONS...] {COMMAND [ARGS...]}

systemd-run [OPTIONS...] [SOCKET OPTIONS...] {COMMAND [ARGS...]}

systemd-run [OPTIONS...] [TIMER OPTIONS...] {COMMAND [ARGS...]}

โœจ DESCRIPTION โœจ

So like, systemd-run is how you create and start a transient .service or .scope unit and run the COMMAND you gave it inside that unit. It can also create and start a transient .path, .socket, or .timer unit โ€” and that unit activates a .service unit when it elapses. Iconic multitasking behavior honestly ๐Ÿ‘‘

If you run a command as a transient service unit, it gets started and managed by the service manager like any other service, which means it shows up in systemctl list-units like any other unit. Bestie it runs in a clean and detached execution environment, with the service manager as its parent process. In this mode, systemd-run starts the service asynchronously in the background and returns right after the command has begun execution โ€” unless --no-block, --wait, --pipe, or --pty are specified (more on those below, iykyk).

If you run a command as a transient scope unit, it gets executed by systemd-run itself as parent process, so it inherits the execution environment of the caller. But the processes of the command are still managed by the service manager, similar to normal services, and they do show up in the output of systemctl list-units. In this case execution is synchronous, and it returns only when the command finishes. You turn this mode on with the --scope switch (see below ๐Ÿซถ).

If you run a command with path, socket, or timer options like --on-calendar= (see below), a transient path, socket, or timer unit is created alongside the service unit for the specified command. Only the transient path, socket, or timer unit is started immediately โ€” the transient service unit gets triggered by that path, socket, or timer unit. And if the --unit= option is specified, the COMMAND may be omitted. In that case, systemd-run creates only a .path, .socket, or .timer unit that triggers the specified unit. No command, no problem ๐Ÿ’โ™€๏ธ

By default, services created with systemd-run default to the simple type โ€” check the description of Type= in systemd.service(5) for the details. But โš ๏ธ note this: when this type is used, the service manager (and therefore the systemd-run command) considers service start-up successful as soon as the fork() for the main service process succeeded, i.e. before the execve() is invoked โ€” which means it counts as success even if the specified command can't actually be started. So if you want systemd-run to return successfully only when the specified command line has genuinely been started, consider using the exec service type, i.e. --property=Type=exec. Do that. Trust ๐ŸŒธ

After systemd-run passes the command to the service manager, the manager performs variable expansion. Which means: dollar characters ("$") that should not be expanded need to be escaped as "$$". You can also just turn expansion off entirely with --expand-environment=no. Periodt ๐Ÿ’…


๐ŸŽ€ OPTIONS ๐ŸŽ€

The following options are understood (there are a LOT, hydrate first bestie):

  • --scope โ€” Create a transient .scope unit instead of the default transient .service unit (see above ๐Ÿ‘†).

Added in version 206.

  • --unit=, -u โ€” Use this unit name instead of an automatically generated one. Name it yourself, main character energy.

Added in version 206.

  • --property=, -p โ€” Sets a property on the scope or service unit that is created. This option takes an assignment in the same format as systemctl(1)'s set-property command.

Added in version 211.

  • --description= โ€” Provide a description for the service, scope, path, socket, or timer unit. If you don't specify one, the command itself gets used as the description. See Description= in systemd.unit(5).

Added in version 206.

  • --slice= โ€” Make the new .service or .scope unit part of the specified slice, instead of system.slice (when running in --system mode) or the root slice (when running in --user mode).

Added in version 206.

  • --slice-inherit โ€” Make the new .service or .scope unit part of the slice that systemd-run itself has been invoked in. You can combine this with --slice=, and in that case the slice specified via --slice= is placed within the slice the systemd-run command was invoked in.

Example bestie: say systemd-run is invoked in the slice foo.slice, and the --slice= argument is bar. The unit will then be placed under foo-bar.slice. Slay.

Added in version 246.

  • --expand-environment=BOOL โ€” Expand environment variables in command arguments. If enabled (that's the default), environment variables specified as "${VARIABLE}" get expanded the same way as in commands specified via ExecStart= in units. With --scope, this expansion is performed by systemd-run itself, and in other cases by the service manager that spawns the command. Note that this is similar to โ€” but not the same as โ€” variable expansion in bash(1) and other shells.

See systemd.service(5) for a description of variable expansion. Disabling variable expansion is useful if the specified command includes, or may include, a "$" sign.

Added in version 254.

  • -r, --remain-after-exit โ€” After the service process has terminated, keep the service around until it is explicitly stopped. Super useful for collecting runtime information about the service after it finished running. Also see RemainAfterExit= in systemd.service(5). ๐Ÿšซ This option may not be combined with --wait, --scope, --pty, --pty-late, or --pipe.

Added in version 207.

  • --send-sighup โ€” When terminating the scope or service unit, send a SIGHUP immediately after SIGTERM. This is useful to indicate to shells and shell-like processes that the connection has been severed. Also see SendSIGHUP= in systemd.kill(5).

Added in version 207.

  • --service-type= โ€” Sets the service type. Also see Type= in systemd.service(5). This option has no effect in conjunction with --scope. Defaults to simple.

Added in version 211.

  • --uid=, --gid= โ€” Runs the service process under the specified UNIX user and group. Also see User= and Group= in systemd.exec(5).

Added in version 211.

  • --nice= โ€” Runs the service process with the specified nice level. Also see Nice= in systemd.exec(5).

Added in version 211.

  • --working-directory= โ€” Runs the service process with the specified working directory. Also see WorkingDirectory= in systemd.exec(5).

Added in version 240.

  • --same-dir, -d โ€” Similar to --working-directory=, but uses the current working directory of the caller for the service to execute. "wherever I am, you are" vibes ๐Ÿฅบ

Added in version 240.

  • --root-directory= โ€” Runs the service process with the specified root directory. Also see RootDirectory= in systemd.exec(5).

โš ๏ธ Note that the path is looked up inside the file system namespace that systemd-run is running in, which might be different than the file system namespace the manager process is running in. Use the RootDirectory= property directly if you want the path to be looked up in the manager process's file system namespace.

Added in version 259.

  • --same-root-dir, -R โ€” Similar to --root-directory=, but uses the root directory of the systemd-run process as the root directory to execute the service in.

Added in version 259.

  • -E NAME[=VALUE], --setenv=NAME[=VALUE] โ€” Runs the service process with the specified environment variable set. You can use this parameter more than once to set multiple variables. When "=" and VALUE are omitted, the value of the variable with the same name in the program environment will be used.

Also see Environment= in systemd.exec(5).

Added in version 211.

  • --pty, -t โ€” When invoking the command, the transient service connects its standard input, output and error to the terminal systemd-run is invoked on, via a pseudo TTY device. This lets you run programs that expect interactive user input/output as services โ€” like interactive command shells, bestie ๐Ÿ’…

This option will result in systemd-run synchronously waiting for the transient service to terminate, similar to specifying --wait. If specified along with --wait, systemd-run will not exit when you manually disconnect from the pseudo TTY device.

Note that machinectl(1)'s shell command is usually a better alternative for requesting a new, interactive login session on the local host or a local container. Just saying ๐Ÿซถ

See below for details on how this switch combines with --pipe.

Added in version 219.

  • --pty-late, -T โ€” Much like --pty, but the PTY communication only begins once the unit start is complete. Effectively this means PTY input during ExecStartPre= of a service is not available while using --pty-late, while it is available using --pty, for most service types. However, if this option is used, access to the invoking TTY is available for password queries (see --no-ask-password below) during unit start-up. Trade-offs, bestie ๐Ÿ”ฅ

Added in version 258.

  • --pipe, -P โ€” If specified, standard input, output, and error of the transient service are inherited from the systemd-run command itself. This lets systemd-run be used within shell pipelines. Pipes? Pipes. ๐Ÿงš

Note that this mode is not suitable for interactive command shells and similar, because the service process will not become a TTY controller when invoked on a terminal. Use --pty instead in that case.

When both --pipe and --pty are used in combination, the more appropriate option is automatically determined and used. Specifically: when invoked with standard input, output and error connected to a TTY, --pty is used, and otherwise --pipe.

This option will result in systemd-run synchronously waiting for the transient service to terminate, similar to specifying --wait.

When this option is used, the original file descriptors systemd-run receives are passed to the service processes as-is. If the service runs with different privileges than systemd-run, this means the service might not be able to reopen the passed file descriptors, due to normal file descriptor access restrictions. If the invoked process is a shell script that uses the echo "hello" >/dev/stderr construct for writing messages to stderr, this might cause problems, as this only works if stderr can be reopened. To mitigate this, use the construct echo "hello" >&2 instead, which is mostly equivalent and avoids this pitfall. Write it down bestie, this one bites people ๐Ÿ“

Added in version 235.


(part 1 done โ€” more coming, don't go anywhere ๐Ÿฆ‹)

  • --shell, -S โ€” bestie, this is the "just gimme a shell" button ๐Ÿ’… It's a shortcut for --pty --same-dir --wait --collect --service-type=exec $SHELL, aka you get an interactive shell, in the current working directory, running in service context, and you only had to type one (1) switch to get it. One switch!! (added in version 240 โœจ)
  • --quiet, -q โ€” shhh ๐Ÿคซ suppresses extra informational output while it's running. Super handy when you pair it with --pty, because that's exactly the case where it kills the initial message explaining how to terminate the TTY connection. (added in version 219 โœจ)
  • -v, --verbose โ€” shows you the unit log output while the thing is running. Not after, while. ๐Ÿ‘€ (added in version 258 โœจ)
  • --output= โ€” controls how that verbose unit log output gets formatted. Possible values live over in journalctl(1), so go check there. (added in version 261 โœจ)
  • --on-active=, --on-boot=, --on-startup=, --on-unit-active=, --on-unit-inactive= โ€” these define a monotonic timer relative to different starting points, for when the command you specified should start. The deets are in OnActiveSec=, OnBootSec=, OnStartupSec=, OnUnitActiveSec= and OnUnitInactiveSec= in systemd.timer(5). Each one of these is secretly a shortcut for --timer-property= with the matching property, iykyk. ๐Ÿ•
โš ๏ธ Heads up: these may not be combined with --scope or --pty. Not a vibe, just the rules.

(added in version 218 โœจ)

  • --on-calendar= โ€” sets up a calendar timer for starting your command. See OnCalendar= in systemd.timer(5). This one's a shortcut for --timer-property=OnCalendar=. ๐Ÿ“…
Same warning as above: may not be combined with --scope or --pty.

(added in version 218 โœจ)

  • --on-clock-change, --on-timezone-change โ€” these define a trigger based on system clock jumps or timezone changes, so your command fires when time does a little chaos dance. See OnClockChange= and OnTimezoneChange= in systemd.timer(5). They're shortcuts for --timer-property=OnClockChange=yes and --timer-property=OnTimezoneChange=yes respectively. ๐Ÿ•ฐ๏ธ
Yep, you guessed it: may not be combined with --scope or --pty.

(added in version 242 โœจ)

  • --path-property=, --socket-property=, --timer-property= โ€” sets a property on the path, socket, or timer unit that gets created. It's giving --property=, except it applies to the transient path, socket, or timer unit instead of the transient service unit. It takes an assignment in the same format as the set-property command from systemctl(1). ๐ŸŽ€
And yes, once more with feeling: these may not be combined with --scope or --pty.

(added in version 218 โœจ)

  • --no-block โ€” do not synchronously wait for the unit start operation to finish. Here's the tea: if you don't pass this, the start request for the transient unit gets verified, enqueued, and then systemd-run sits there waiting until the unit's start-up is completed. If you do pass it, the request is only verified and enqueued, and off it goes. ๐Ÿš€
May not be combined with --wait or --scope.

(added in version 220 โœจ)

  • --wait โ€” synchronously waits for the transient service to terminate. So the start request gets verified, enqueued, and waited for. Then the invoked unit is monitored, and it's waited on until it's deactivated again (most likely because the command you gave it finished, bestie). On exit you get terse info about the unit's runtime โ€” total runtime, plus CPU, memory, IO, and IP accounting data if the corresponding cgroup accounting settings are enabled โ€” and the exit code and status of the main process. You can mute all that with --quiet. ๐Ÿ”ฅ
May not be combined with --no-block, --remain-after-exit, --scope, or the various path, socket, or timer options.

(added in version 232 โœจ)

  • -G, --collect โ€” unloads the transient unit after it completed, even if it failed. Because normally (i.e. without this option) every unit that ran and failed is kept in memory until you explicitly reset its failure state with systemctl reset-failed or an equivalent command โ€” meanwhile units that ran successfully get unloaded immediately. Turn this on and the "garbage collection" of units gets way more aggressive, unloading units regardless of whether they exited successfully or failed. ๐Ÿ—‘๏ธ It's a shortcut for --property=CollectMode=inactive-or-failed; the full explanation of CollectMode= is chilling in systemd.unit(5). (added in version 236 โœจ)
  • --job-mode=MODE โ€” when a new job gets queued, this controls how to deal with jobs that are already queued. It takes the same mode values as systemctl(1)'s --job-mode= option. The default job mode is "fail". And if you run --job-mode=help it'll show you a list of all the available job modes. ๐Ÿ“‹ (added in version 258 โœจ)
  • --ignore-failure โ€” by default, if the command you specified fails, the invoked unit gets marked failed (though it might still be unloaded, see --collect=, above) and that gets reported in the logs. Pass this switch and that reporting is suppressed, and any non-success exit status/code of the command is treated as success. ๐Ÿ˜Œ
Not supported in --scope mode, bestie.

(added in version 256 โœจ)

  • --background=COLOR โ€” changes the terminal background color to the ANSI color you specify, for as long as the session lasts. ๐ŸŽจ The color you give should be an ANSI X3.64 SGR background color, i.e. strings like "40", "41", ..., "47", "48;2;...", "48;5;...". See ANSI Escape Code (Wikipedia)[1] for the full lore. (added in version 256 โœจ)
  • --user โ€” talks to the service manager of the calling user, instead of the service manager of the system. Your own little kingdom ๐Ÿ‘‘
  • --system โ€” talks to the service manager of the system. This is the implied default, so if you don't say anything, this is what you get. ๐Ÿ–ฅ๏ธ
  • -H, --host= โ€” execute the operation remotely. Give it a hostname, or a username and hostname separated by "@", to connect to. The hostname may optionally be suffixed by the port ssh is listening on, separated by ":", and then a container name, separated by "/", which connects you straight to a specific container on that host. This uses SSH to talk to the remote machine manager instance. ๐ŸŒ You can enumerate container names with machinectl -H HOST. And put IPv6 addresses in brackets, ok? Don't forget, that's a whole thing.
  • -M, --machine= โ€” execute the operation on a local container. Give it a container name to connect to, optionally prefixed by a user name to connect as, plus a separating "@" character. If you use the special string ".host" in place of the container name, a connection to the local system is made โ€” which is useful for connecting to a specific user's user bus: --user --machine=lennart@.host. ๐Ÿซถ If the "@" syntax isn't used, the connection is made as root user. If the "@" syntax is used, either the left hand side or the right hand side may be omitted (but not both), in which case the local user name and ".host" are implied.
  • -C, --capsule= โ€” execute the operation on a capsule. Give it a capsule name to connect to. ๐ŸงŠ See capsule@.service(5) for all the details about capsules. (added in version 256 โœจ)
  • --no-ask-password โ€” do not query the user for authentication for privileged operations. No pop-ups, no drama. ๐Ÿ™ˆ
  • -h, --help โ€” prints a short help text and exits. Classic. ๐Ÿ’–
  • --version โ€” prints a short version string and exits. Also classic. โญ
  • --json=MODE โ€” shows output formatted as JSON. It expects one of "short" (the shortest possible output, no redundant whitespace or line breaks), "pretty" (a pretty version of that same thing, with indentation and line breaks), or "off" (turns JSON output off โ€” this is the default). ๐ŸŒˆ
  • --no-pager โ€” do not pipe output into a pager. Currently this only applies to --help. (The pager is not started during normal operation, so don't stress.) (added in version 258 โœจ)
๐Ÿšจ Important compatibility tea: --json= may not be combined with --scope, --pty, --pty-late, --pipe, --verbose, or path, socket, or timer options. Pick a lane, bestie.

Also, a lil note that matters a LOT: all command line arguments after the first non-option argument become part of the command line of the launched process. So everything after that first non-option thing? That's the launched process's business now. ๐Ÿซก


๐Ÿ’ฅ EXIT STATUS ๐Ÿ’ฅ

Okay so, on success, 0 is returned. โœจ

If systemd-run failed to start the service, you get a non-zero return value back. ๐Ÿ˜ญ

And if systemd-run waits for the service to terminate, the return value gets propagated from the service. 0 is returned on success โ€” and that includes all the cases where systemd considers a service to have exited cleanly. See the discussion of SuccessExitStatus= in systemd.service(5) for the whole breakdown. ๐Ÿซถ

๐Ÿ’– EXAMPLES ๐Ÿ’–

ok bestie this is the part where it all clicks. real commands, real output, no cap. ๐Ÿซถ


โœจ Example 1. Logging environment variables provided by systemd to services

watch what happens when you just run env under systemd-run โ€” it gets its own unit, its own journal, its whole moment:

# systemd-run env
Running as unit: run-19945.service
# journalctl -u run-19945.service
Sep 08 07:37:21 bupkis systemd[1]: Starting /usr/bin/env...
Sep 08 07:37:21 bupkis systemd[1]: Started /usr/bin/env.
Sep 08 07:37:21 bupkis env[19948]: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Sep 08 07:37:21 bupkis env[19948]: LANG=en_US.UTF-8
Sep 08 07:37:21 bupkis env[19948]: BOOT_IMAGE=/vmlinuz-3.11.0-0.rc5.git6.2.fc20.x86_64

that's it. the environment variables systemd handed to the service are just right there in the journal. iconic behavior honestly. ๐Ÿ’…


โœจ Example 2. Limiting resources available to a command

# systemd-run -p IOWeight=10 updatedb

so this one runs the updatedb(8) tool, but it lowers the block I/O weight for it down to 10. wanna know more about the IOWeight= property? go read systemd.resource-control(5). she explains it way better than me. ๐ŸŽ€


โœจ Example 3. Running commands at a specified time

the following command will touch a file after 30 seconds. yes, like a little timed surprise:

# date; systemd-run --on-active=30 --timer-property=AccuracySec=100ms /bin/touch /tmp/foo
Mon Dec  8 20:44:24 KST 2014
Running as unit: run-71.timer
Will run service as unit: run-71.service
# journalctl -b -u run-71.timer
-- Journal begins at Fri 2014-12-05 19:09:21 KST, ends at Mon 2014-12-08 20:44:54 KST. --
Dec 08 20:44:38 container systemd[1]: Starting /bin/touch /tmp/foo.
Dec 08 20:44:38 container systemd[1]: Started /bin/touch /tmp/foo.
# journalctl -b -u run-71.service
-- Journal begins at Fri 2014-12-05 19:09:21 KST, ends at Mon 2014-12-08 20:44:54 KST. --
Dec 08 20:44:48 container systemd[1]: Starting /bin/touch /tmp/foo...
Dec 08 20:44:48 container systemd[1]: Started /bin/touch /tmp/foo.

notice there's a timer unit AND a service unit. two units, one job. main character energy for real. โฐโœจ


โœจ Example 4. Allowing access to the tty

this one invokes bash(1) as a service, and it passes its standard input, output and error straight to the calling TTY:

# systemd-run -t --send-sighup bash

that's the whole thing. your terminal, your shell, but it's a service now. slay. ๐Ÿš


โœจ Example 5. Start screen as a user service

$ systemd-run --scope --user screen
Running scope as unit run-r14b0047ab6df45bfb45e7786cc839e76.scope.

$ screen -ls
There is a screen on:
        492..laptop     (Detached)
1 Socket in /var/run/screen/S-fatima.

okay so what's actually happening here: this starts the screen process as a child of the systemd --user process that was started by user@.service, inside a scope unit. and here's the why, bestie: a systemd.scope(5) unit is used instead of a systemd.service(5) unit, because screen will exit when detaching from the terminal, and a service unit would be terminated. ๐Ÿ’€

running screen as a user unit has the advantage that it is not part of the session scope. and heads up โ€” if KillUserProcesses=yes is configured in logind.conf(5) (it defaults to "no"), the session scope will be terminated when the user logs out of that session.

the user@.service is started automatically when the user first logs in, and stays around as long as at least one login session is open. after the user logs out of the last session, user@.service and all services underneath it are terminated. this behavior is the default, when "lingering" is not enabled for that user. enabling lingering means that user@.service is started automatically during boot, even if the user is not logged in, and that the service is not terminated when the user logs out.

enabling lingering allows the user to run processes without being logged in โ€” for example, to allow screen to persist after the user logs out, even if the session scope is terminated. and in the default configuration, users can enable lingering for themselves:

$ loginctl enable-linger

periodt. no sudo, no drama. ๐ŸŒธ


โœจ Example 6. Variable expansion by the manager

$ systemd-run -t echo "<${INVOCATION_ID}>" '<${INVOCATION_ID}>'
      <> <5d0149bfa2c34b79bccb13074001eb20>

okay this one is SO satisfying, watch closely: the first argument is expanded by the shell (double quotes), but the second one is not expanded by the shell (single quotes). echo(1) is called with ["/usr/bin/echo", "<>", "<${INVOCATION_ID}>"] as the argument array, and then systemd(1) generates ${INVOCATION_ID} and substitutes it in the command-line.

and the reason this substitution could not be done on the client side? because the target ID that will be set for the service is not known before the call is made. mystery until the last second. ๐Ÿ’…โœจ


โœจ Example 7. Variable expansion and output redirection using a shell

variable expansion by systemd(1) can be disabled with --expand-environment=no.

disabling variable expansion can be useful if the command to execute contains dollar characters and escaping them would be inconvenient. for example, when a shell is used:

$ systemd-run --expand-environment=no -t bash \
      -c 'echo $SHELL $$ >/dev/stdout'
/bin/bash 12345

the last argument is passed verbatim to the bash(1) shell which is started by the service unit. the shell expands "$SHELL" to the path of the shell, and "$$" to its process number, and then those strings are passed to the echo built-in and printed to standard output (which, in this case, is connected to the calling terminal). chef's kiss. ๐Ÿ‘‘๐Ÿฆ‹


โœจ Example 8. Return value

$ systemd-run --user --wait true
$ systemd-run --user --wait -p SuccessExitStatus=11 bash -c 'exit 11'
$ systemd-run --user --wait -p SuccessExitStatus=SIGUSR1 --expand-environment=no \
      bash -c 'kill -SIGUSR1 $$'

those three invocations will succeed, i.e. terminate with an exit code of 0. all three. no notes. ๐ŸŒˆโญ


๐ŸŒŸ SEE ALSO ๐ŸŒŸ

your reading list, bestie, go forth and hyperlink-hop:

systemd(1), systemctl(1), systemd.unit(5), systemd.service(5), systemd.scope(5), systemd.slice(5), systemd.exec(5), systemd.resource-control(5), systemd.timer(5), systemd-mount(1), machinectl(1), run0(1)


๐ŸŽ€ NOTES ๐ŸŽ€

  1. ANSI Escape Code (Wikipedia) https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters

and that's the whole page, bestie!! ๐Ÿฅน๐Ÿ’– this beauty is from systemd 262.1, and the page itself is SYSTEMD-RUN(1). now go run something in a transient unit and tell me all about it. you're gonna do amazing. periodt. ๐Ÿ’…โœจ

๐Ÿ“ฆ where this page comes from
  • arch rolling: systemd 262.1-1 ยท LGPL-2.1-or-later AND CC0-1.0 AND GPL-2.0-or-later AND MIT-0
  • ๐Ÿ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms