It is completely possible to create and use s6/s6-rc services as a local user without any elevated privileges at all. It just takes a little bit of setup.

Note: It is possible to do do this on any init system. One could run openrc as their init and setup a local s6/s6-rc user process if desired by the user. If your init system already has some way of managing users services (like suite66 or dinit), you probably don't need this guide.

Initial Setup

If you are not running s6 or suite66 as your init, start off by installing s6-rc.

 pacman -S s6-rc

This command will pull all the needed dependencies. It will not replace your init or anything of that nature either.

Next, you need some place to keep source directories for services. This guide will assume ~/.local/share/s6 (XDG_DATA_DIR). Create an rc and sv subdirectory.

~/.local/share

 └── s6
      ├── rc
      └── sv

Example Services

As an example, we will setup udiskie, a local user daemon for auto-mounting usb devices. First, make the directory.

 mkdir ~/.local/share/s6/sv/udiskie

Then inside the directory we need a run and type file. They look like this.

~/.local/share/s6/sv/udiskie/run

 #!/bin/execlineb -P
 exec udiskie

~/.local/share/s6/sv/udiskie/type

 longrun

This guide uses execline for the run script because it is a lightweight, non-interactive language designed to work well with s6 and s6-rc. However, you are free to use any scripting language you'd like with s6-rc. As long as the shebang is valid, it will work. The single exception is that a oneshot script must be execline because of how s6-rc internally works. However, this script can simply be a one line call to another script (i.e. exec sh /path/to/shell/script) so there is no limitation in practice.

Now lets also make a bundle called default that contains udiskie and possibly any other services so they can all be brought up with a single command.

 mkdir -p ~/.local/share/s6/sv/default/contents.d
 touch ~/.local/share/s6/sv/default/contents.d/udiskie
 echo "bundle" > ~/.local/share/s6/sv/default/type

Note: Check upstream documentation on source directories and service directories for full details on what you can put in here.

Create the s6-rc database

Now that the services have been written, the s6-rc database is ready to be created.

 s6-rc-compile ~/.local/share/s6/rc/compiled-$(date +%s) ~/.local/share/s6/sv

This command takes two arguments. The first is simply the path to the database, and the second is the path of the source directories. The databases name is in the format of compiled-$(date +%s). That command merely gives a unix timestamp of the current time to ensure that the database name is unique. The databases can have any name, but it is highly recommended to use something that generates unique names everytime a databse is created.

Now that the database exists, the symlink needs to be created. Remember that symlinks must be absolute paths. Replace timestamp below with whatever the database actually is.

 ln -sf /home/${USER}/.local/share/s6/rc/compiled-timestamp /home/${USER}/.local/share/s6/rc/compiled

Important note: If the compiled symlink already exists, the new symlink must be made atomically. The default ln does not do this. This will be omitted in this guide but consult the upstream documentation on database management for more details.

This process can be tedious to do manually, but luckily it can easily be scripted. Artix currently uses a very similar script for managing s6 services in the repos. Adjust the below to fit your needs.

 #!/bin/sh

 DATAPATH="/home/${USER}/.local/share/s6"
 RCPATH="${DATAPATH}/rc"
 DBPATH="${RCPATH}/compiled"
 SVPATH="${DATAPATH}/sv"
 SVDIRS="/tmp/${USER}/s6-rc/servicedirs"
 TIMESTAMP=$(date +%s)

 if ! s6-rc-compile "${DBPATH}"-"${TIMESTAMP}" "${SVPATH}"; then
     echo "Error compiling database. Please double check the ${SVPATH} directories."
     exit 1
 fi

 if [ -e "/tmp/${USER}/s6-rc" ]; then
     for dir in "${SVDIRS}"/*; do
         if [ -e "${dir}/down" ]; then
             s6-svc -x "${dir}"
         fi
     done
    s6-rc-update -l "/tmp/${USER}/s6-rc" "${DBPATH}"-"${TIMESTAMP}"
 fi

 if [ -d "${DBPATH}" ]; then
     ln -sf "${DBPATH}"-"${TIMESTAMP}" "${DBPATH}"/compiled && mv -f "${DBPATH}"/compiled "${RCPATH}"
 else
     ln -sf "${DBPATH}"-"${TIMESTAMP}" "${DBPATH}"
 fi

 echo "==> Switched to a new database for ${USER}."
 echo "    Remove any old unwanted/unneeded database directories in ${RCPATH}."

Simply run that script as a local user and it will handle all database updates/upgrades automatically.

Complete s6 supervision (optional)

This requires that s6/s6-rc is being used as the main init system. The local user services can be plugged into the overall supervision tree of the system. This ensures that local user services are fully supervised by s6.

This section requires root/adminstrator privileges. First, make a config file for ease of use. /etc/s6/config/user-services.conf

 # username for the user-services bundle
 USER=username

Create a bundle called user-services.

 mkdir -p /etc/s6/adminsv/user-services/contents.d
 touch /etc/s6/adminsv/user-services/local-s6-user
 touch /etc/s6/adminsv/user-services/local-s6-rc-user
 echo "bundle" > /etc/s6/adminsv/user-services/type

For s6-rc to work, an s6-svscan process needs to be running. Since this is for the local user, make sure all of the commands in this script are run as the local user. Additionally, a scan directory needs to be picked for s6-svscan to use. It needs to be something the local user has full read/write access to. In this example, the /tmp/${USER}/service directory will be used. Upstream recommends having this be a RAM filesystem (such as tmpfs) and it works the best with s6-rc. Here are the details.

 mkdir -p /etc/s6/adminsv/local-s6-user/dependencies.d
 touch /etc/s6/adminsv/local-s6-user/dependencies.d/mount-filesystems
 touch /etc/s6/adminsv/local-s6-user/dependencies.d/mount-tmpfs
 echo "3" > /etc/s6/adminsv/local-s6-user/notification-fd
 echo "longrun" > /etc/s6/adminsv/local-s6-user/type

/etc/s6/adminsv/local-s6-user/run

 #!/bin/execlineb -P
 envfile /etc/s6/config/user-services.conf
 importas -uD "username" USER USER
 foreground { install -d -o ${USER} -g ${USER} /tmp/${USER} }
 foreground { install -d -o ${USER} -g ${USER} /tmp/${USER}/service }
 s6-setuidgid ${USER} exec s6-svscan -d 3 /tmp/${USER}/service

While this script does parse the conf file for the USER variable, note that the username part allows for a fallback USER in case the envfile fails somehow. Take advantage of it to put the desired user in there.

Now finally, it is time for the local-s6-rc-user piece. This is merely a oneshot that runs after local-s6-user starts.

 mkdir -p /etc/s6/adminsv/local-s6-rc-user/dependencies.d
 touch /etc/s6/adminsv/local-s6-rc-user/mount-filesystems
 touch /etc/s6/adminsv/local-s6-rc-user/mount-tmpfs
 touch /etc/s6/adminsv/local-s6-rc-user/local-s6-user
 echo "oneshot" > /etc/s6/adminsv/local-s6-rc-user/type

/etc/s6/adminsv/local-s6-rc-user/down

 #!/bin/execlineb -P
 envfile /etc/s6/config/user-services.conf
 importas -uD "username" USER USER
 foreground { s6-setuidgid ${USER} s6-rc -l /tmp/${USER}/s6-rc -bDa change }
 foreground { s6-setuidgid ${USER} rm -r /tmp/${USER}/service }
 s6-setuidgid ${USER}
 elglob -0 dirs /tmp/${USER}/s6-rc*
 forx -E dir { ${dirs} }
 	rm -r ${dir}

/etc/s6/adminsv/local-s6-rc-user/up

 #!/bin/execlineb -P
 envfile /etc/s6/config/user-services.conf
 importas -uD "username" USER USER
 foreground { s6-setuidgid ${USER}
 s6-rc-init -c /home/${USER}/.local/share/s6/rc/compiled -l /tmp/${USER}/s6-rc /tmp/${USER}/service }
 s6-setuidgid ${USER}
 exec s6-rc -l /tmp/${USER}/s6-rc -up change default

The same note about username applies here.

The root/administrator database can finally be updated.

  s6-db-reload

That's it. The local s6-rc database bundle can be started like any other service.

  sudo s6-rc -u change user-services

The really nice thing about this setup is that the local s6-svscan process is completely supervised. It will never die during the lifetime of the machine unless you purposely tell it to die. If you blindly give the PID a kill command, the s6-supervise process for it will simply respawn it. Your user can continue to use s6-rc and s6 commands on their local services as normal. These scripts are fairly generic you so if you want to add more users, you can basically just copy and paste and just change a few paths/variable names and add them to the user-services bundle. To get these services to always start on boot, just add user-services to your default bundle in the root database and you are good to go.

Standalone local s6 supervision process

If the local s6 user services did/could not be plugged into the adminstrator/root supervision tree, then this process can be used instead. Otherwise, skip this section.

For s6-svscan to work, it needs some scan directory. For this example, some folders in /tmp will be used since it is a RAM filesystem that any user can easily write to.

 mkdir /tmp/${USER}
 mkdir /tmp/${USER}/service
 s6-svscan /tmp/${USER}/service

This will leave s6-svscan running in the foreground which is intended. In another terminal, start s6-rc.

 s6-rc-init -c /home/${USER}/.local/share/s6/rc/compiled -l /tmp/${USER}/s6-rc /tmp/${USER}/service
 s6-rc -l /tmp/${USER}/s6-rc -up change default

That's it. As long as the s6-svscan process is running, you have a fully local instance of s6-rc to use.

Local s6 user service usage

It works almost exactly the same as the normal s6-rc commands. The difference is that the -l argument needs to be supplied in order to point to the correct database. In the case of udiskie, you can start it like so:

 s6-rc -l /tmp/${USER}/s6-rc -u change udiskie

To bring down the user's default bundle, this would work.

 s6-rc -l /tmp/${USER}/s6-rc -d change default

Note that all these commands are run locally without any root/adminstrator privileges at all.

Inheriting environment variables

If you are running the local s6 user services from the overall root supervision tree, you might be surprised to find out that your local user services will have no environment variables to speak of. This is because s6/s6-rc is designed to run in clean, reproducible environments and PID1, of course, has no environment variables. When it comes to local user daemons, it is not uncommon that they would require some sort of environment variable in order to function. Luckily, variables can easily be inherited from the s6-svscan process. Here's some tips to make it easier.

Recall the /etc/s6/config/user-services.conf file from the previous section. Edit it and put some more variables in there.

 # environment variables for the local s6-rc database
 DISPLAY=:0
 UID=1000
 USER=username

Adjust that as needed. If execline is being used for scripts (recommended), only key=value pairs are allowed. More complicated variable definitions can be saved for the actual runscript.

Now edit the /etc/s6/adminsv/local-s6-user/run file.

 envfile /etc/s6/config/user-services.conf
 importas -i UID UID
 importas -i USER USER
 export HOME /home/${USER}
 export XDG_RUNTIME_DIR /run/user/${UID}

 foreground { install -d -o ${USER} -g ${USER} /tmp/${USER} }
 foreground { install -d -o ${USER} -g ${USER} /tmp/${USER}/service }
 s6-setuidgid ${USER} exec s6-svscan -d 3 /tmp/${USER}/service

Now, the HOME, UID, USER, and XDG_RUNTIME_DIR variables are available for any of your local user services. They all inherit it from s6-svscan. To access them in your scripts, it's just one importas command. For example:

 #!/bin/execlineb -P
 importas -i USER USER
 exec xrdb /home/${USER}/.Xresources