How SDKs compare to Dockerfiles

Workshop didn’t occur in a vacuum; there have been many attempts to provide developers with robust environments. A common approach is to use Docker to achieve repeatability, persistence, layering, and various other benefits that the technology offers.

We won’t dwell on the pros and cons here; instead, let’s discuss how a typical Dockerfile development environment maps to a workshop and its SDKs.

Note

We assume you’re familiar with SDKcraft basics covered in the Craft SDKs with SDKcraft tutorial section and have an understanding of Docker.

Feature discussion

To begin with, it’s perfectly reasonable to draw a few comparisons between Docker and the combination of Workshop and SDKcraft.

(Im)mutability

The first contrast comes from the overall approach: Docker images are conceived to be immutable, whereas workshops are designed to evolve over time. This affects all aspects of their design and implementation, including how Dockerfiles and SDKs are laid out, respectively.

Bind mounts and volumes

Docker provides several ways to manage data persistence and storage such as the VOLUME instructions, the docker volume command or the --mount and -v options in docker run. The expectations for their configuration are set by the image author but the actual parameters are provided by users at the author’s guidance; the resulting manual process is error-prone and adds unnecessary overhead.

Workshop and SDKcraft reciprocate this with mount interface plugs that are akin to Docker volumes and the workshop remount command that enables remounting existing plugs to a given location. However, the user can’t create arbitrary mounts; the choice is limited to what the SDKs offer.

In turn, this implies that the mount logic in Workshop and SDKcraft is built into the SDK by its author, not implemented manually by the user; unless the user decides to intervene, the mounts are managed automatically and largely stay hidden.

Resource usage

For largely historical reasons, the Docker way of accessing various host resources can be notably inconsistent; for example, enabling GPU pass-through is visibly different from SSH forwarding.

In contrast, Workshop and SDKcraft unify these mechanisms under the single concept of an interface, providing a consistent way to uniformly manage host resource access.

Parts and layers

Docker relies on a temporally layered approach, where each change is built on top of the previous one.

Our SDKs are structured using parts; their expressiveness makes them more diverse and semantically rich, allowing the layout of an SDK to be formalized in a modular way. If necessary, the layered approach can be mimicked using SDK hooks. Workshop uses ZFS snapshots and clones to cache the results of each setup-base hook.

Build commands

In Docker, build commands are typically bundled as RUN instructions.

In SDKcraft SDKs, the setup-base hook is responsible for building the workshop, but other hooks add extra functionality with runtime events and health checks.

A related Docker pattern applies USER to switch from root to a nonroot user, followed by RUN instructions for user-level setup:

# System setup as root (≈ setup-base)
RUN apt-get update && apt-get install -y ...

# Switch to non-root user and set up the project (≈ setup-project)
USER appuser
WORKDIR /home/appuser
RUN pip install --user ...

In Workshop, this maps to the setup-project hook, which inherently runs as the workshop user. Unlike setup-base, setup-project runs after interfaces are connected and the /project/ directory is mounted, so it can leverage available hardware and install project-specific dependencies.

Data persistence and sharing

Consider this Docker command:

$ docker run --name share-example --entrypoint bash -it \
  -v ~/docker/kit/cache/Kit:/kit/cache:rw \
  -v ~/docker/cache/ov:/root/.cache/ov:rw \
  ...

All too familiar, isn’t it? When running a sufficiently complex container, you need to mount a lot of directories to make it work, and the handling of these mounts both inside and outside the container can quickly become an overhead.

Workshop addresses this issue by providing a way to reuse and share content between the host and the workshop via SDKs while keeping manual intervention to a necessary minimum. Typically, workshops are isolated from each other and from the host system; all data exchange is via the mount interface.

To use this interface, your SDK defines a mount interface plug. When a workshop uses the SDK, an auto-assigned, noncustomizable source directory on the host is mounted to the plug-defined target directory inside the workshop. What’s more, its contents are preserved during refresh operations. In this way, Workshop enables SDK data persistence and reuse inside individual workshops.

Note, however, that files created in the plug’s target location by any means will only be accessible to the workshop to which that specific auto-assigned source directory is mounted to. Other workshops, even if they use the same SDK, cannot access these files and will not share them; their source directories will be different.

Persistence and reuse between workshops

This is the simplest scenario; you use the mount interface to define the target directory where the content will be mounted inside the workshop per each directory you want to retain during the workshop’s lifecycle.

sdkcraft.yaml
name: data-science
title: Data science SDK
base: ubuntu@22.04
summary: This SDK does some data science.
description: |
  Besides doing actual data science,
  this SDK demonstrates content sharing and persistence between workshops
  by enabling two plugs that can store reusable data specific to the SDK.

plugs:
  share-cache:
    interface: mount
    workshop-target: /opt/cache

  training-data:
    interface: mount
    workshop-target: /opt/training
    read-only: true

This SDK defines two mount plugs; for each, Workshop creates a source directory on the host at runtime. Both workshop-target directories inside the workshop can be used by the SDK-specific logic implemented via hooks and other features.

Additionally, you can mark a directory as read-only. Workshop will then enforce the immutability of resources in this directory when they are accessed from inside the workshop.

Here’s a corresponding workshop definition:

.workshop/data.yaml
name: data
base: ubuntu@22.04
sdks:
  - name: data-science

The default host location that Workshop mounts to the target is predefined as follows:

$XDG_DATA_HOME/workshop/id/<PROJECT ID>/<WORKSHOP>/mount/<SDK>/<PLUG>/

In the above example, this would be ~/.local/share/workshop/id/<PROJECT ID>/<WORKSHOP>/mount/data-science/share-cache/. In particular, this means that the SDK’s plug in each workshop will have its own unique source directory.

Share custom host content with a workshop

One issue that the previous scenario doesn’t address is customizing the source directory of a plug. The docker run example at the beginning illustrates this approach; it explicitly lists the host directories to be mounted to each target.

This can also be done with Workshop, and the workshop remount command is the key to it:

$ workshop remount data/data-science:share-cache ~/.local/cache/

This mounts a specific source location on the host, ~/.local/cache/, to the target directory of the share-cache mount interface plug under the data-science SDK in the data workshop defined above.

Feature mapping

Any attempt at a straightforward comparison of these different, albeit vaguely similar, technologies is mostly futile. Again, a key difference is that a Dockerfile is controlled by the user, but a workshop is managed by the user, yet it relies on publisher-defined SDKs whose layout is beyond the user’s reach.

This means that some capabilities of Docker won’t be available to a user of Workshop alone, so the functionality is split between the user-oriented Workshop and the publisher-focused SDKcraft.

Important Dockerfile instructions are mapped to SDKcraft as follows:

Dockerfile

SDKcraft

ADD

parts, mount interface

CMD

setup-base hook

COPY

setup-base hook

ENTRYPOINT

setup-base hook

FROM

base in the SDK definition

HEALTHCHECK

check-health hook

ONBUILD

setup-base hook

RUN

setup-base, setup-project hooks

USER

setup-project hook

VOLUME

mount interface

In turn, the CLI subcommands can be mapped like this:

Docker CLI

Workshop/SDKcraft CLI

docker build

sdkcraft build, sdkcraft pack

docker exec

workshop exec, workshop shell, workshop run

docker images, docker ps

workshop info, workshop list

docker logs

workshop changes, workshop tasks

docker rm, docker rmi

workshop remove

docker run

workshop launch, workshop refresh

docker run --mount, docker volume

workshop remount

docker start

workshop start

docker stop

workshop stop

Case study: ROS 2

For a specific example, consider the Docker-based tutorial for ROS 2, the open-source robotics operating system. The choice is influenced by many factors, including the fact that we have a ROS 2 SDK available for comparison; for details, refer to the corresponding how-to guide under See also.

Nonetheless, we won’t focus on the specifics of ROS 2 here; instead, we discuss how certain parts of an arbitrarily sophisticated Dockerfile map to a similar SDK and the workshop that uses it.

Base image

The example suggests using the ros:rolling tag for the Dockerfile; with a few levels of indirection, it comes down to this (or similar) instruction:

FROM ubuntu:noble

For Workshop and SDKcraft, this translates to ubuntu@24.04 in the SDK definition and the workshop definition.

Project workspace

The project workspace in the example is defined as a bind mount that eventually becomes this:

$ docker run -it \
  --mount type=bind,source=/home/user/ros-project,target=/home/ws/src,consistency=cached \
  # ...

Its counterpart in Workshop is the project directory where the workshop was defined and launched; it is automatically mounted as /project/ when the workshop is started:

$ workshop launch ros2jazzy  # must be run in the project directory

No explicit configuration is needed; this behavior is intentionally consistent across all workshops.

Bind mounts

The ROS 2 example defines a few more mounts; a complete docker run command may look like this:

$ docker run -it \
  --name ros2_container \
  --mount type=bind,source=/home/user/ros-project,target=/home/ws/src,consistency=cached \
  --mount type=bind,source=/home/user/.ros,target=/root/.ros,consistency=cached \
  --mount type=bind,source=/tmp/.X11-unix,target=/tmp/.X11-unix,consistency=cached \
  --mount type=bind,source=/dev/dri,target=/dev/dri,consistency=cached \
  ros2

In Workshop and SDKcraft, additional filesystem mounts are defined by the SDK author or the user using the mount interface:

sdkcraft.yaml
plugs:
  ros-cache:
    interface: mount
    workshop-target: /home/workshop/.ros
# ...

Just like with the project files, this avoids the need for manual setup when starting the workshop:

$ workshop launch ros2jazzy  # the plugs are mounted automatically

Again, Workshop and SDKcraft have no direct counterpart to bind mounts; plugs are more similar to Docker volumes. Yet, the workshop remount command enables remounting existing plugs to new host directories:

$ workshop remount ros2jazzy/ros2:ros-cache ~/new-cache-mount/

Thus, Workshop and SDKcraft largely leave the design of mount points to the SDK author, allowing the user to rely on their default, well-defined behavior with the extra option of adjusting them if necessary.

Build commands

Normally, a RUN instruction in a Dockerfile translates to the setup-base and setup-project hooks in an SDK pretty well. Here, the steps to set up keys, then configure the repos and install the packages largely stay the same.

However, setup-project runs with the project directory already mounted, so any steps that rely on the contents of the project itself can be implemented with the same hook. In particular, this enables the ROS 2 SDK to transparently identify and install project-specific dependencies.

See also

Explanation:

Reference: