Skip to main content

Packages with customizable images

You should move away from having a default image inside main actions: for example, actions such as go are not as flexible and efficient as golangci.

Two reasons:

  1. Default values cannot be directly appended to a composite action
  2. Even if you override a default image with a custom one, the default will still be evaluated and executed, which is wasteful

Three main action patterns

  1. There should be a version without a default image
  2. There should be an image definition (e.g. bash.#Image)
  3. There should be a variation of the main action with the #Image already set up

For the purpose of the guide, let's create a simple, customizable Python action.

1. Main action without a default image

Each main action should be defined without a default image: users will be required to provide it during usage.

package python

// Simple wrapper to run a python script in a container
#RunBase: {
// directory containing script to run
directory: dagger.#FS

// name of script to execute
filename: string

// arguments to the script
args: [...string]

// where to mount the script inside the container
_mountpoint: "/run/python"

docker.#Run & {
command: {
name: "python"
// string concatenation of _mountpoint and filename variables
"args": ["\(_mountpoint)/\(filename)"] + args

mounts: "Python script": {
contents: directory
// where to mount the script inside the container
dest: _mountpoint

The above #RunBase action is a composite action: we inherit the fields of docker.#Run and only require the end-user to specify directory, filename and args.

Using these inputs, we prefill some of the docker.#Run's field to automatically run the script with the inputs.


As it is a composition, the #RunBase action inherits the input field from the docker.#Run action.

However, we never fill it. This is intended, as we want to enforce a design pattern around main actions with customizable images.

2. The image definition

There should be an image definition (e.g. python.#Image) that can easily be used with the #RunBase action, for the most common cases.

package python

// python image
// nothing is configurable
#Image: docker.#Pull & {
source: "python:latest"

But if you're adding a lot of configuration just to tweak the image source:

package python

// python image
// more configurable than the previous one
#ConfigurableImage: {
repository: string | *"python"
tag: string | *"latest"

docker.#Pull & {
source: "\(repository):\(tag)"

Then users should probably just use another custom image instead:

package python

// custom image built with a pip3 package
#CustomImage: docker.#Build & {
steps: [
alpine.#Build & {
packages: {
bash: version: "=~5.1"
"py3-pip": _
"python3-dev": _
bash.#Run & {
script: contents: "pip3 install ANY_PACKAGE"

You can see that the first python.#Image is pretty basic. It doesn't contain any configuration, and is the expected, minimal image that creators of main actions shall include in their packages.


It is not the responsibility of the action's creator to make the image as modular as possible: a simple version covering 90% of the most common use-cases is the only requirement

3. Variation with the image already set up

Each point builds upon the last, from flexibility to simplified usage.

package python

// Like #RunBase, but with a pre-configured container image.
#Run: #RunBase & {
_image: #Image
image: _image.output

This variation implements a ready-to-be-used action where no image is required. It will not fit all the use-cases, but will be users of this action gain time.