DEV Community

Ákos Takács
Ákos Takács

Posted on • Edited on

Using gVisor's container runtime in Docker Desktop

Introduction

I compared 3 container runtimes previously, which included runsc from gVisor. Since the default is runc, that is the default in Docker Desktop as well. The Kata runtime could not be tested in Docker Desktop, since that would require nested virtualization in the VM of Docker Desktop, so runsc is the only runtime from that comparison we could try in Docker Desktop which isn't already in it.

The only question is how we could copy the runtime binary into Docker Desktop. This could also be asked in general related to any binary, since the root filesystem of Docker Desktop is read-only, but the runtime is executed by Docker so copying that would actually make sense.

Table of contents

Download runsc into Docker Desktop

» Back to table of contents «

If a runtime can be downloaded directly without using any package manager, you can download it to a location which can be read by the Docker daemon. There is one thing that we know the Docker daemon can read, and we can write, and that is a local volume. So we will need a container that downloads runsc to a local volume.

First we will need to check the official documentation for an installation guide. Despite that the documentation mentions installing specific releases as well, I couldn't figure it out and the link they provide to the releases on GitHub shows that there is no release at all, so we will install the latest version.

I used the script from the documentation, except that I removed the parenthesis and I also needed to remove sudo from the beginning of the last line. We don't need it, since we will use the root user in the container. I will use the script directly in a compose file, so I had to escape all the dollar characters with a second dollar character. This is the compose file:

services:
  runsc-installer:
    image: alpine:3.20
    volumes:
      - type: volume
        source: data
        target: /usr/local/bin
    command:
      - sh
      - -c
      - |
        set -e
        ARCH=$(uname -m)
        URL=https://storage.googleapis.com/gvisor/releases/release/latest/$${ARCH}
        wget $${URL}/runsc $${URL}/runsc.sha512 \
          $${URL}/containerd-shim-runsc-v1 $${URL}/containerd-shim-runsc-v1.sha512
        sha512sum -c runsc.sha512 \
          -c containerd-shim-runsc-v1.sha512
        rm -f *.sha512
        chmod a+rx runsc containerd-shim-runsc-v1
        mv runsc containerd-shim-runsc-v1 /usr/local/bin

volumes:
  data:
    name: runsc-runtime-binaries
Enter fullscreen mode Exit fullscreen mode

The chance of you using runsc-runtime-binaries as a volume for something else is very low. But if you do have it, make sure you use a different volume name.

Place the file anywhere as compose.yml and run

docker compose run --rm runsc-installer
Enter fullscreen mode Exit fullscreen mode

You will see the progress which is something like this:

Connecting to storage.googleapis.com (172.217.19.123:443)
saving to 'runsc'
runsc                100% |****************************************************************| 60.2M  0:00:00 ETA
'runsc' saved
Connecting to storage.googleapis.com (172.217.19.123:443)
saving to 'runsc.sha512'
runsc.sha512         100% |****************************************************************|   136  0:00:00 ETA
'runsc.sha512' saved
Connecting to storage.googleapis.com (172.217.19.123:443)
saving to 'containerd-shim-runsc-v1'
containerd-shim-runs 100% |****************************************************************| 27.7M  0:00:00 ETA
'containerd-shim-runsc-v1' saved
Connecting to storage.googleapis.com (172.217.19.123:443)
saving to 'containerd-shim-runsc-v1.sha512'
containerd-shim-runs 100% |****************************************************************|   155  0:00:00 ETA
'containerd-shim-runsc-v1.sha512' saved
runsc: OK
containerd-shim-runsc-v1: OK
Enter fullscreen mode Exit fullscreen mode

Whenever a new version comes out, you can run the command again, and it will replace the old files.

Configure the Docker daemon to use the runtime

» Back to table of contents «

As I also mentioned in Everything about Docker volumes, you can inspect a volume and see the mount point

docker volume inspect runsc-runtime-binaries
Enter fullscreen mode Exit fullscreen mode

This is my output:

[
    {
        "CreatedAt": "2024-10-28T17:21:24Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "03-gvisor-in-docker-desktop-data",
            "com.docker.compose.version": "2.29.7",
            "com.docker.compose.volume": "data"
        },
        "Mountpoint": "/var/lib/docker/volumes/runsc-runtime-binaries/_data",
        "Name": "runsc-runtime-binaries",
        "Options": null,
        "Scope": "local"
    }
]
Enter fullscreen mode Exit fullscreen mode

Normally a mount point would be where you mount something not from where you mounted it, but I guess it is called mount point because of what I described in the linked tutorial, since unless you are using the default local volumes, this is where the data could be mounted to. Now that we know that, the following script can be used to add the runtime to the daemon config:

#!/usr/bin/env bash

volume="runsc-runtime-binaries"
volume_path="/var/lib/docker/volumes/$volume/_data"

docker run -it --rm \
  --mount "type=bind,source=$HOME/.docker,target=/etc/docker" \
  --mount "type=volume,source=$volume,target=$volume_path" \
  ubuntu "$volume_path/runsc" install
Enter fullscreen mode Exit fullscreen mode

This script also mounts $HOME/.docker to /etc/docker in the container, since it contains the daemon.json and runsc install will add the parameters to /etc/docker/daemon.json by default and back up the original file as daemon.json~. If you save the script as runsc-install.sh, this is how you could run it:

chmod +x ./runsc-install.sh
./runsc-install.sh
Enter fullscreen mode Exit fullscreen mode

This script would work only on Linux and macOS, and we would have other problems as well, so before doing too much this way, let's jump to the next section and introduce Docker Compose.

Download and install runsc in one step

» Back to table of contents «

If we want to make the installer simpler, we can add the "install" command to the compose file. This means that we have to mount $HOME/.docker in the compose file. We should also remove the last line of the original script in the compose file, because we don't want to move the binaries to /usr/local/bin anymore, since runsc install will need to run from a different folder, and we need only one mount for the volume. So we change the mount definition of the volume too and we also want a platform independent way to refer to our home folder so we replace $HOME with the tilde character (~), which actually came as a surprise to me, but it works in the compose file even on Windows. And finally, we run the installer. So these are the last three lines:

dest="/var/lib/docker/volumes/${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}/_data/"
mv runsc containerd-shim-runsc-v1 $$dest
$$dest/runsc install
Enter fullscreen mode Exit fullscreen mode

Notice that $dest is escaped with a second dollar character but $RUNSC_VOLUME_NAME isn't. This is because the volume name will be a compose parameter with a default value so it has to be interpreted by compose. This is the full compose file:

services:
  runsc-installer:
    image: alpine:3.20
    volumes:
      - type: volume
        source: data
        target: /var/lib/docker/volumes/${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}/_data/
      - type: bind
        source: ~/.docker
        target: /etc/docker
    command:
      - sh
      - -c
      - |
        set -e
        ARCH=$(uname -m)
        URL=https://storage.googleapis.com/gvisor/releases/release/latest/$${ARCH}
        wget $${URL}/runsc $${URL}/runsc.sha512 \
          $${URL}/containerd-shim-runsc-v1 $${URL}/containerd-shim-runsc-v1.sha512
        sha512sum -c runsc.sha512 \
          -c containerd-shim-runsc-v1.sha512
        rm -f *.sha512
        chmod a+rx runsc containerd-shim-runsc-v1

        dest="/var/lib/docker/volumes/${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}/_data/"
        mv runsc containerd-shim-runsc-v1 $$dest
        $$dest/runsc install
volumes:
  data:
    name: ${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}
Enter fullscreen mode Exit fullscreen mode

This way you can even change the volume name. and run the compose project like this on Linux and macOS (or on Windows, with WSL2 integration enabled, and running docker commands in your WSL distribution):

RUNSC_VOLUME_NAME="runsc-bin" docker compose run --rm runsc-installer 
Enter fullscreen mode Exit fullscreen mode

My previous examples used docker compose run, but you can use docker compose up as well. Then the log lines will be prefixed with the container name and the container will be kept after running it.

Testing runsc after the installation

» Back to table of contents «

Before you test the runtime, don't change any Docker Desktop configuration, because that will reset your client config on the host, since the VM still doesn't know about it. In order to avoid resetting the config file, "Quit Docker Desktop" and then start it again. DO NOT just "Restart" it in the menu, because that will just restart services in the VM without updating the config.

When the config is ready and Docker Desktop is started again, you can run

docker run --rm --runtime runsc ubuntu uname -a
Enter fullscreen mode Exit fullscreen mode

If everything works, you will see something like this:

Linux 73309f6efa2c 4.4.0 #1 SMP Sun Jan 10 15:06:54 PST 2016 aarch64 aarch64 aarch64 GNU/Linux
Enter fullscreen mode Exit fullscreen mode

For more testing ideas, check out my blog post about the comparison of runtimes.

Reloading the internal daemon config to support Windows

» Back to table of contents «

I already mentioned some ways to support Windows, but there is one more problem. When using the WSL2 backend, Docker Desktop on Windows supports Nvidia GPUs, because WSL2 supports it too. Unfortunately, it seems that the Nvidia runtime configuration currently completely overrides the "runtimes" section in the daemon config instead of adding itself to the existing "runtimes" section.

That means, even if we add runsc in the daemon config on the host, it will not be usable You will see it even in Docker Desktop's graphical interface, but it will not actually be the one that is used by the Docker daemon. So I will show a workaround I came up with, until it is fixed in Docker Desktop if it is ever considered a bug to be fixed. Let's not forget that there is nothing to indicate that using a custom runtime is supported in Docker Desktop. Just because we can do something, it doesn't mean we will always be able to do it if it is not supported. So maybe it will be fixed in the future, but it is also possible, that nothing that I described in this blogpost will work in the future. With that in mind, here is the workaround:

  • We have to find the config file used by the docker daemon in the virtual machine
  • We have to mount it from the VM into our installer container
  • We have to find a way to pass a custom config path to the runsc install command and automatically update the internal config file
  • After updating the internal config file, we have to reload it, which means, we have to send a HUP signal to the dockerd process.
  • In order to be able to send signals to the dockerd process, we need to use the process namespace of the host.

The first can be done by running the following command:

docker run --rm -it --pid host alpine:3.20 sh -c 'ps aux | grep dockerd | grep -v grep'
Enter fullscreen mode Exit fullscreen mode

This should show something like the following:

308 root 0:45 /usr/local/bin/dockerd --config-file /run/config/docker/daemon.json --containerd /run/containerd/containerd.sock --pidfile /run/desktop/docker.pid --swarm-default-advertise-addr=192.168.65.3 --host-gateway-ip 192.168.65.254

So we know, the config is at /run/config/docker/daemon.json. Let's check the content by running the following command on Windows

docker run -v /run/config/docker/daemon.json:/daemon.json alpine:3.20 cat /daemon.json
Enter fullscreen mode Exit fullscreen mode

The output is something like this:

{"builder":{"gc":{"defaultKeepStorage":"20GB","enabled":true}},"experimental":true,"features":{"cone-registries":["hubproxy.docker.internal:5555"],"mtu":1500,"runtimes":{"nvidia":{"path":"nvidia-contain-driver":"overlayfs","userland-proxy":false}

This indeed shows the nvidia container runtime.

We could also download "runsc" to somewhere or use the one on the already created volume to run runsc install -help, or we can check the source code too: https://github.com/google/gvisor/blob/745828301c936ddd1dac49b2611bdf1a4477f9ab/runsc/cmd/install.go#L60

This shows we have a config_file option. Great. Now we need to notify the dockerd process that it should reload the daemon config, which requires some Linux experience, but the pkill command can help us like this:

pkill -HUP dockerd
Enter fullscreen mode Exit fullscreen mode

This will work only if we add pid: host to the installer service.

Note that this will notify all dockerd processes, so if you have Docker in Docker, it will reload all Docker daemon's configuration. It shouldn't be a problem unless you broke the config of any docker daemon. The new compose file is the following now:

services:
  runsc-installer:
    image: alpine:3.20
    volumes:
      - type: volume
        source: data
        target: /var/lib/docker/volumes/${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}/_data/
      - type: bind
        source: ~/.docker
        target: /etc/docker
      - type: bind
        source: /run/config/docker/daemon.json
        target: /run/config/docker/daemon.json
    pid: host
    command:
      - sh
      - -c
      - |
        set -e
        ARCH=$(uname -m)
        URL=https://storage.googleapis.com/gvisor/releases/release/latest/$${ARCH}
        wget $${URL}/runsc $${URL}/runsc.sha512 \
          $${URL}/containerd-shim-runsc-v1 $${URL}/containerd-shim-runsc-v1.sha512
        sha512sum -c runsc.sha512 \
          -c containerd-shim-runsc-v1.sha512
        rm -f *.sha512
        chmod a+rx runsc containerd-shim-runsc-v1

        dest="/var/lib/docker/volumes/${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}/_data/"
        mv runsc containerd-shim-runsc-v1 $$dest
        $$dest/runsc install

        $$dest/runsc install -config_file /run/config/docker/daemon.json
        pkill -HUP dockerd
volumes:
  data:
    name: ${RUNSC_VOLUME_NAME:-runsc-runtime-binaries}
Enter fullscreen mode Exit fullscreen mode

Using this compose file, you can even test runsc on Windows, but you will have to run the installer every single time you restart Docker Desktop.

Possible error messages

» Back to table of contents «

If something goes wrong and runsc cannot be found, you can see an error message like below, which I got when I accidentally wrote "volume" instead of "volumes" in the script, so Docker tried to find the runtime using an incorrect path.

docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: unable to retrieve OCI runtime error (open /var/run/desktop-containerd/daemon/io.containerd.runtime.v2.task/moby/3a01e11f99939f9fd85151e2ae97f15d5f39ff94e835187357b82807147dcc52/log.json: no such file or directory): fork/exec /var/lib/docker/volume/runsc-runtime-binaries/_data/runsc: no such file or directory: <nil>: unknown.

Without the automatic reloading of the internal daemon config, if you try the runtime too early before activating it in the VM, for example because your config was reset to the previous one after you changed something in Docker Desktop before stopping it, you could get an error about the invalid runtime name:

docker: Error response from daemon: unknown or invalid runtime name: runsc.
See 'docker run --help'.
Enter fullscreen mode Exit fullscreen mode

Conclusion

Changing the runtime in Docker Desktop is not the first thing that Docker Desktop users think. It is probably not even in the top 10, but sometimes, it can be useful if you have only Docker Desktop at the moment. So this post was basically just a way to know more about Docker Desktop, Docker Compose, and how we can change the daemon configuration manually, or automatically.

If you break the daemon configuration, you can fix it, but you have to quit Docker Desktop and start it again.

You could also learn a little bit about volumes, but if you need more, you can read the already mentioned Everything about Docker volumes

And finally, it is always good to understand general Linux commands even when we use Docker Desktop on Windows, but we want to run Linux containers. It helps us to troubleshoot and do things that are not officially supported yet, but can be done at the moment.

Docker Desktop is not Docker, as I pointed it out in "You run containers, not dockers - Discussing Docker variants, components and versioning", which means there are some things that you can do with Docker Desktop but not with Docker CE, or you can do something with Docker CE but not with Docker Desktop. I still like to find the limits of both so we can enjoy as many features of both as possible.

Top comments (0)