diff --git a/meson.build b/meson.build
index 734a2897558..426bd32991c 100644
--- a/meson.build
+++ b/meson.build
@@ -1673,6 +1673,14 @@ if conf.get('ENABLE_LOGIND') == 1
         endif
 endif
 
+executable('systemd-user-runtime-dir',
+           user_runtime_dir_sources,
+           include_directories : includes,
+           link_with : [libshared, liblogind_core],
+           install_rpath : rootlibexecdir,
+           install : true,
+           install_dir : rootlibexecdir)
+
 if conf.get('HAVE_PAM') == 1
         executable('systemd-user-sessions',
                    'src/user-sessions/user-sessions.c',
diff --git a/src/login/logind-user.c b/src/login/logind-user.c
index 01b602dbc49..712067c52c7 100644
--- a/src/login/logind-user.c
+++ b/src/login/logind-user.c
@@ -319,55 +319,6 @@ int user_load(User *u) {
         return r;
 }
 
-static int user_mkdir_runtime_path(User *u) {
-        int r;
-
-        assert(u);
-
-        r = mkdir_safe_label("/run/user", 0755, 0, 0, MKDIR_WARN_MODE);
-        if (r < 0)
-                return log_error_errno(r, "Failed to create /run/user: %m");
-
-        if (path_is_mount_point(u->runtime_path, NULL, 0) <= 0) {
-                _cleanup_free_ char *t = NULL;
-
-                r = asprintf(&t, "mode=0700,uid=" UID_FMT ",gid=" GID_FMT ",size=%zu%s",
-                             u->uid, u->gid, u->manager->runtime_dir_size,
-                             mac_smack_use() ? ",smackfsroot=*" : "");
-                if (r < 0)
-                        return log_oom();
-
-                (void) mkdir_label(u->runtime_path, 0700);
-
-                r = mount("tmpfs", u->runtime_path, "tmpfs", MS_NODEV|MS_NOSUID, t);
-                if (r < 0) {
-                        if (!IN_SET(errno, EPERM, EACCES)) {
-                                r = log_error_errno(errno, "Failed to mount per-user tmpfs directory %s: %m", u->runtime_path);
-                                goto fail;
-                        }
-
-                        log_debug_errno(errno, "Failed to mount per-user tmpfs directory %s, assuming containerized execution, ignoring: %m", u->runtime_path);
-
-                        r = chmod_and_chown(u->runtime_path, 0700, u->uid, u->gid);
-                        if (r < 0) {
-                                log_error_errno(r, "Failed to change runtime directory ownership and mode: %m");
-                                goto fail;
-                        }
-                }
-
-                r = label_fix(u->runtime_path, 0);
-                if (r < 0)
-                        log_warning_errno(r, "Failed to fix label of '%s', ignoring: %m", u->runtime_path);
-        }
-
-        return 0;
-
-fail:
-        /* Try to clean up, but ignore errors */
-        (void) rmdir(u->runtime_path);
-        return r;
-}
-
 static int user_start_service(User *u) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         char *job;
@@ -414,15 +365,9 @@ int user_start(User *u) {
          */
         u->stopping = false;
 
-        if (!u->started) {
+        if (!u->started)
                 log_debug("Starting services for new user %s.", u->name);
 
-                /* Make XDG_RUNTIME_DIR */
-                r = user_mkdir_runtime_path(u);
-                if (r < 0)
-                        return r;
-        }
-
         /* Save the user data so far, because pam_systemd will read the
          * XDG_RUNTIME_DIR out of it while starting up systemd --user.
          * We need to do user_save_internal() because we have not
@@ -483,29 +428,6 @@ static int user_stop_service(User *u) {
         return r;
 }
 
-static int user_remove_runtime_path(User *u) {
-        int r;
-
-        assert(u);
-
-        r = rm_rf(u->runtime_path, 0);
-        if (r < 0)
-                log_error_errno(r, "Failed to remove runtime directory %s (before unmounting): %m", u->runtime_path);
-
-        /* Ignore cases where the directory isn't mounted, as that's
-         * quite possible, if we lacked the permissions to mount
-         * something */
-        r = umount2(u->runtime_path, MNT_DETACH);
-        if (r < 0 && !IN_SET(errno, EINVAL, ENOENT))
-                log_error_errno(errno, "Failed to unmount user runtime directory %s: %m", u->runtime_path);
-
-        r = rm_rf(u->runtime_path, REMOVE_ROOT);
-        if (r < 0)
-                log_error_errno(r, "Failed to remove runtime directory %s (after unmounting): %m", u->runtime_path);
-
-        return r;
-}
-
 int user_stop(User *u, bool force) {
         Session *s;
         int r = 0, k;
@@ -555,11 +477,6 @@ int user_finalize(User *u) {
                         r = k;
         }
 
-        /* Kill XDG_RUNTIME_DIR */
-        k = user_remove_runtime_path(u);
-        if (k < 0)
-                r = k;
-
         /* Clean SysV + POSIX IPC objects, but only if this is not a system user. Background: in many setups cronjobs
          * are run in full PAM and thus logind sessions, even if the code run doesn't belong to actual users but to
          * system components. Since enable RemoveIPC= globally for all users, we need to be a bit careful with such
diff --git a/src/login/meson.build b/src/login/meson.build
index a616d5b934d..1e2cf49c431 100644
--- a/src/login/meson.build
+++ b/src/login/meson.build
@@ -58,6 +58,11 @@ loginctl_sources = files('''
         sysfs-show.c
 '''.split())
 
+user_runtime_dir_sources = files('''
+        user-runtime-dir.c
+        logind.h
+'''.split())
+
 if conf.get('ENABLE_LOGIND') == 1
         logind_conf = configure_file(
                 input : 'logind.conf.in',
diff --git a/src/login/user-runtime-dir.c b/src/login/user-runtime-dir.c
new file mode 100644
index 00000000000..1bb26c99e48
--- /dev/null
+++ b/src/login/user-runtime-dir.c
@@ -0,0 +1,170 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <stdint.h>
+#include <sys/mount.h>
+
+#include "fs-util.h"
+#include "label.h"
+#include "logind.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "smack-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+static int gather_configuration(size_t *runtime_dir_size) {
+        Manager m = {};
+        int r;
+
+        manager_reset_config(&m);
+
+        r = manager_parse_config_file(&m);
+        if (r < 0)
+                log_warning_errno(r, "Failed to parse logind.conf: %m");
+
+        *runtime_dir_size = m.runtime_dir_size;
+        return 0;
+}
+
+static int user_mkdir_runtime_path(const char *runtime_path, uid_t uid, gid_t gid, size_t runtime_dir_size) {
+        int r;
+
+        assert(runtime_path);
+        assert(path_is_absolute(runtime_path));
+        assert(uid_is_valid(uid));
+        assert(gid_is_valid(gid));
+
+        r = mkdir_safe_label("/run/user", 0755, 0, 0, MKDIR_WARN_MODE);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create /run/user: %m");
+
+        if (path_is_mount_point(runtime_path, NULL, 0) >= 0)
+                log_debug("%s is already a mount point", runtime_path);
+        else {
+                char options[sizeof("mode=0700,uid=,gid=,size=,smackfsroot=*")
+                             + DECIMAL_STR_MAX(uid_t)
+                             + DECIMAL_STR_MAX(gid_t)
+                             + DECIMAL_STR_MAX(size_t)];
+
+                xsprintf(options,
+                         "mode=0700,uid=" UID_FMT ",gid=" GID_FMT ",size=%zu%s",
+                         uid, gid, runtime_dir_size,
+                         mac_smack_use() ? ",smackfsroot=*" : "");
+
+                (void) mkdir_label(runtime_path, 0700);
+
+                r = mount("tmpfs", runtime_path, "tmpfs", MS_NODEV|MS_NOSUID, options);
+                if (r < 0) {
+                        if (!IN_SET(errno, EPERM, EACCES)) {
+                                r = log_error_errno(errno, "Failed to mount per-user tmpfs directory %s: %m", runtime_path);
+                                goto fail;
+                        }
+
+                        log_debug_errno(errno, "Failed to mount per-user tmpfs directory %s.\n"
+                                        "Assuming containerized execution, ignoring: %m", runtime_path);
+
+                        r = chmod_and_chown(runtime_path, 0700, uid, gid);
+                        if (r < 0) {
+                                log_error_errno(r, "Failed to change ownership and mode of \"%s\": %m", runtime_path);
+                                goto fail;
+                        }
+                }
+
+                r = label_fix(runtime_path, 0);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to fix label of \"%s\", ignoring: %m", runtime_path);
+        }
+
+        return 0;
+
+fail:
+        /* Try to clean up, but ignore errors */
+        (void) rmdir(runtime_path);
+        return r;
+}
+
+static int user_remove_runtime_path(const char *runtime_path) {
+        int r;
+
+        assert(runtime_path);
+        assert(path_is_absolute(runtime_path));
+
+        r = rm_rf(runtime_path, 0);
+        if (r < 0)
+                log_error_errno(r, "Failed to remove runtime directory %s (before unmounting): %m", runtime_path);
+
+        /* Ignore cases where the directory isn't mounted, as that's
+         * quite possible, if we lacked the permissions to mount
+         * something */
+        r = umount2(runtime_path, MNT_DETACH);
+        if (r < 0 && !IN_SET(errno, EINVAL, ENOENT))
+                log_error_errno(errno, "Failed to unmount user runtime directory %s: %m", runtime_path);
+
+        r = rm_rf(runtime_path, REMOVE_ROOT);
+        if (r < 0)
+                log_error_errno(r, "Failed to remove runtime directory %s (after unmounting): %m", runtime_path);
+
+        return r;
+}
+
+static int do_mount(const char *runtime_path, uid_t uid, gid_t gid) {
+        size_t runtime_dir_size;
+
+        assert_se(gather_configuration(&runtime_dir_size) == 0);
+
+        log_debug("Will mount %s owned by "UID_FMT":"GID_FMT, runtime_path, uid, gid);
+        return user_mkdir_runtime_path(runtime_path, uid, gid, runtime_dir_size);
+}
+
+static int do_umount(const char *runtime_path) {
+        log_debug("Will remove %s", runtime_path);
+        return user_remove_runtime_path(runtime_path);
+}
+
+int main(int argc, char *argv[]) {
+        const char *user;
+        uid_t uid;
+        gid_t gid;
+        char runtime_path[sizeof("/run/user") + DECIMAL_STR_MAX(uid_t)];
+        int r;
+
+        log_parse_environment();
+        log_open();
+
+        if (argc != 3) {
+                log_error("This program takes two arguments.");
+                return EXIT_FAILURE;
+        }
+        if (!STR_IN_SET(argv[1], "start", "stop")) {
+                log_error("First argument must be either \"start\" or \"stop\".");
+                return EXIT_FAILURE;
+        }
+
+        umask(0022);
+
+        user = argv[2];
+        r = get_user_creds(&user, &uid, &gid, NULL, NULL);
+        if (r < 0) {
+                log_error_errno(r,
+                                r == -ESRCH ? "No such user \"%s\"" :
+                                r == -ENOMSG ? "UID \"%s\" is invalid or has an invalid main group"
+                                             : "Failed to look up user \"%s\": %m",
+                                user);
+                return EXIT_FAILURE;
+        }
+
+        xsprintf(runtime_path, "/run/user/" UID_FMT, uid);
+
+        if (streq(argv[1], "start"))
+                r = do_mount(runtime_path, uid, gid);
+        else if (streq(argv[1], "stop"))
+                r = do_umount(runtime_path);
+        else
+                assert_not_reached("Unknown verb!");
+
+        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/units/meson.build b/units/meson.build
index 1c89f369570..fed2f107534 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -218,6 +218,7 @@ in_units = [
          'multi-user.target.wants/'],
         ['systemd-vconsole-setup.service',       'ENABLE_VCONSOLE'],
         ['systemd-volatile-root.service',        ''],
+        ['user-runtime-dir@.service',            ''],
         ['user@.service',                        ''],
 ]
 
diff --git a/units/user-runtime-dir@.service.in b/units/user-runtime-dir@.service.in
new file mode 100644
index 00000000000..8c02beda3bd
--- /dev/null
+++ b/units/user-runtime-dir@.service.in
@@ -0,0 +1,17 @@
+#  SPDX-License-Identifier: LGPL-2.1+
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=/run/user/%i mount wrapper
+StopWhenUnneeded=yes
+
+[Service]
+ExecStart=@rootlibexecdir@/systemd-user-runtime-dir start %i
+ExecStop=@rootlibexecdir@/systemd-user-runtime-dir stop %i
+RemainAfterExit=true
diff --git a/units/user@.service.in b/units/user@.service.in
index 372ffa56d36..b88108e1b75 100644
--- a/units/user@.service.in
+++ b/units/user@.service.in
@@ -10,6 +10,8 @@
 [Unit]
 Description=User Manager for UID %i
 After=systemd-user-sessions.service
+After=user-runtime-dir@%i.service
+Requires=user-runtime-dir@%i.service
 
 [Service]
 User=%i