Adding OS extensions to the host system

Fedora CoreOS keeps the base image as simple and small as possible for security and maintainability reasons. That is why you should in general prefer the usage of podman containers over layering software.

However, in some cases it is necessary to add software to the base OS itself. For example, drivers or VPN software are potential candidates because they are harder to containerize.

To do this, you can use rpm-ostree install. Consider these packages as "extensions": they extend the functionality of the base OS rather than e.g. providing runtimes for user applications. That said, there are no restrictions on which packages one can actually install. By default, packages are downloaded from the Fedora repositories.

To start the layering of a package, you need to write a systemd unit that executes the rpm-ostree command to install the wanted package(s). Changes are applied to a new deployment and a reboot is necessary for those to take effect.

Example: Layering nano and setting it as the default editor

This example shows on how to install the text editor nano and add a script to /etc/profile.d so that it is set as the default editor for all users. The parameter --allow-inactive is useful if the package is added to the root image in a future Fedora CoreOS release. In such a case, the parameter prevents that the service would fail.

In the future, we will have a more Ignition-friendly method of doing this with stronger guarantees. See upstream issues butane#81 and fedora-coreos-tracker#681 for more information.
The After=systemd-machine-id-commit.service directive is important in the following examples to avoid some subtle issues. Similarly, any ConditionFirstBoot=true services should use Before=first-boot-complete.target systemd-machine-id-commit.service. See the systemd documentation for more details.
variant: fcos
version: 1.3.0
systemd:
  units:
    # installing nano as a layered package with rpm-ostree
    - name: rpm-ostree-install-nano.service
      enabled: true
      contents: |
        [Unit]
        Description=Layer nano with rpm-ostree
        # We run after `systemd-machine-id-commit.service` to ensure that
        # `ConditionFirstBoot=true` services won't rerun on the next boot.
        After=systemd-machine-id-commit.service
        After=network-online.target
        # We run before `zincati.service` to avoid conflicting rpm-ostree
        # transactions.
        Before=zincati.service
        ConditionPathExists=!/var/lib/rpm-ostree-install-nano.stamp

        [Service]
        Type=oneshot
        RemainAfterExit=yes
        ExecStart=/usr/bin/rpm-ostree install --allow-inactive nano
        ExecStart=/bin/touch /var/lib/rpm-ostree-install-nano.stamp
        ExecStart=/bin/systemctl --no-block reboot

        [Install]
        WantedBy=multi-user.target
storage:
  files:
    # use nano as default editor
    - path: /etc/profile.d/nano.sh
      overwrite: true
      contents:
        inline: |
          #/bin/sh
          export EDITOR=nano