Dagger 0.12: interactive debugging, reliability and performance, compat mode, corporate networks, and more

July 12, 2024

Jul 12, 2024

Share
Share
Share
Share

Today we are introducing version 0.12 of the Dagger Engine. After several months of shipping bold new features at a breakneck pace (Functions, Daggerverse, Traces) for this release we focused on usability, polish, and quality. This includes a delightful interactive debugger; a more polished terminal and web UI; improved reliability and performance; a backwards compatibility system to make upgrades easier; better support for corporate networks; the ability to host modules on any git server; and much more.

Let’s dive in!

Interactive Debugging

For as long as we can remember, Dagger users wanted to have an interactive shell when their pipeline failed, with all the context at the point of failure. This is similar to a debugger experience, but without needing to set breakpoints explicitly. Other users wanted explicit breakpoints, and the same interactive experience. All these new capabilities are an enrichment to the Terminal type which was first introduced in Dagger v0.10.

As of Dagger v0.12, running dagger call with the –interactive (-i for short) flag will open a terminal in the context of a pipeline failure. No changes to your pipeline code required.

As an alternative to this convenient behavior, Terminal() can now be added anywhere in a pipeline and it will behave like a debugger breakpoint:

Go

func (m *Interactive) Container(ctx context.Context) (string, error) {
    return dag.Container().
        From("alpine:latest").
        Terminal().
        WithExec([]string{"sh", "-c", "echo hello world > /foo && cat /foo"}).
        Terminal().
        Stdout(ctx

Python

@function
async def container(self) -> str:
    return await (
        dag.container()
        .from_("alpine:latest")
        .terminal()
        .with_exec(["sh", "-c", "echo hello world > /foo && cat /foo"])
        .terminal()
        .stdout()
    )

TypeScript

@func()
async container(): Promise<string> {
  return dag
    .container()
    .from("alpine:latest")
    .terminal()
    .withExec(["sh", "-c", "echo hello world > /foo && cat /foo"])
    .terminal()
    .stdout()
}

Properties of this terminal can be customized, including reusing all the layers in a different container base image (e.g. Ubuntu instead of Alpine in the example below):

Go

func (m *Interactive) Dir(ctx context.Context) (string, error) {
    return dag.
        Git("https://github.com/dagger/dagger.git").
        Head().
        Tree().
        Terminal(dagger.DirectoryTerminalOpts{
            Container: dag.Container().From("ubuntu"),
            Cmd:       []string{"bash"},
        }).
        Directory().
        File("README.md").
        Contents(ctx

Python

@function
async def dir(self) -> str:
    return await (
        dag
        .git("https://github.com/dagger/dagger.git")
        .head()
        .tree()
        .terminal(
            container=dag.container().from_("ubuntu"),
            cmd=["bash"]
         )
        .directory(".")
        .file("README.md")
        .contents()
    )

TypeScript

@func()
async dir(): Promise<string> {
  return dag
    .git("https://github.com/dagger/dagger.git")
    .head()
    .tree()
    .terminal({container: dag.container().from("ubuntu"), cmd: ["bash"]})
    .directory(".")
    .file("README.md")
    .contents()
}

For a longer demonstration of how this works in practice, check out this Community Call Demo.

If you are interested in the code changes, check out these pull requests:

UI Polish

One of Dagger’s most popular features is the ability to visualize your pipeline execution in a detailed trace view, both in the terminal, and in a web browser.

This release includes many improvements to both our terminal and web user interfaces

Terminal UI: faster, now interactive

The Terminal UI is now interactive, while still presenting a svelte interface that gets out of your way. You can now use your keyboard and mouse to navigate, zoom in to spans, switch to a web view, and adjust the verbosity at runtime. A keymap is now shown along the bottom that doubles as a divider between the trace tree and the logs of the currently-zoomed span, saving precious screen space.

Additionally, the TUI has seen massive performance improvements. Previously viewing a large trace would burn a ton of CPU simply rendering the TUI and cropping it to fit the viewport. Now the TUI will only render the visible region, on top of many other optimizations.

To see a demonstration of this new feature, check out the demo below:

If you are interested in the code changes, see this pull request: https://github.com/dagger/dagger/pull/7671

Web UI: local view, CI view, flamecharts, async…

A few months ago, we announced Dagger Traces, a web counterpart to our popular terminal UI, to make it even easier to debug and monitor your Dagger pipelines. Since the initial release, we have added numerous improvements:

  • Local view shows all traces from your team’s local dev environments. This is a great way to collaborate with your team, and catch problems early, before changes are even pushed or processed by the CI server

  • CI view shows all traces from your CI environment, grouped by branch and commit, and annotated with useful git metadata

  • Traces include a new flame chart view, which enables clicking on points of interest from the top of the trace. This is especially useful to navigate large and complex traces

  • Better handling of asynchronous operations. Because of Dagger’s lazy execution model, many function calls are asynchronous: the API call to schedule it completes right away, but the actual operation will be executed later. This can make it difficult to connect “cause” from “effect” when viewing a complex trace. To improve readability, we now show “effects” alongside their “cause” in the trace, which is more intuitive.

  • We have made trace views faster overall, with even more performance improvements planned

  • General UI streamlining; less confusing lines, more noticeable focus indicator, less noise

  • Fixed various paper cuts: text selection issues, better handling of timeouts, removed confusing git metadata from local traces

You can watch a demo of all the recent improvements & additions here:

Corporate Network Support

Many users run their Dagger engines in corporate environments or other specialized networks that require use of custom certificate authorities or HTTP proxies.

The engine now supports configuring these proxies and custom CAs. This applies to the engine’s built-in operations (pulling container images, git repos, etc.), user containers created by `withExec` APIs and Module Function containers.

This support unblocks users with these specialized needs who previously could often not use Dagger at all. In particular, the support for automatically applying these settings to user containers and Module Functions means that users get this support “automagically” in their pipelines without needing to customize their Dagger code or any external Dagger module dependencies.

Further improvements to these features are planned, such as improved support for application specific proxy settings and configuration sourced from client machines.

You can learn more about how to configure these settings in our docs.

Reliability and Performance Improvements

We have made Dagger faster and more reliable for large and complex pipelines.

We have done this by fixing a long tail of issues in complex production pipelines, including the ones we use to develop Dagger itself. These issues are often difficult to reproduce, and typically involve  64-192 vCPU hosts, heavy-duty caching, pipeline parallelism, and highly diverse workloads. It’s painstaking work, but each production issue fixed makes the platform more robust for everyone. Thank you to our operators community for their invaluable feedback and collaboration! If you are running Dagger in production and have questions or feedback, we would love to hear from you on our Discord server.

We have been shipping these improvements gradually, in patch releases, typically a few releases per month. If you have been updating frequently, this may have gone unnoticed since the improvements were incremental, usually in the core of Dagger Engine, which is not something that many users notice in our changelog. Here is a more detailed list of relevant changes:

Compatibility Mode

We are introducing Compatibility Mode, a new feature that improves backwards compatibility of the Dagger Engine by simulating the behavior of older engines. This allows you to upgrade to the latest version of Dagger with confidence, knowing that your dependencies will still work, even if they target older versions.

At Dagger we believe that backwards compatibility is a crucial component of a successful software ecosystem. This is especially true for a young and fast-moving platform like Dagger: at our current rate of improvement, API changes cannot be entirely avoided. Compatibility Mode will allow us to improve backwards compatibility over time, without sacrificing the velocity which makes the Dagger community so special.

Note that Compatibility Mode still has limitations. Most importantly, we don’t guarantee 100% backwards compatibility, although we aim to get there eventually. We also don’t provide feature granularity: modules can’t pick and choose which API changes they upgrade, and which they don’t. We plan on adding this feature later, when the scale and maturity of the platform justifies it.

If you are interested in the code changes, check out these pull requests:

Modules on any Git server

We have removed a limitation which made it less practical to host Dagger Modules on some Git servers. Now, all Git servers are supported equally. This includes GitHub, GitLab, Bitbucket, and any other managed or self-hosted Git server.

Here is an example of calling the familiar “hello” module, hosted on Codeberg by a Dagger team member:

dagger -m codeberg.org/gerhard/daggerverse/hello call hello

One of the benefits of Dagger is to free your CI/CD pipelines from excessive platform lock-in, whenever possible. This improvement gets us one step closer to achieving this goal.

If you are interested in the code changes, see this pull request: https://github.com/dagger/dagger/pull/7511 

Enum support

By popular demand, Dagger SDKs now support custom enumeration types. This is useful for restricting possible values for an argument.

Go

// Vulnerability severity levels
type Severity string

const (
    // Undetermined risk; analyze further.
    Unknown Severity = "UNKNOWN"
        
    // Minimal risk; routine fix.
    Low Severity = "LOW"
        
    // Moderate risk; timely fix.
    Medium Severity = "MEDIUM"
    
    // Serious risk; quick fix needed.
    High Severity = "HIGH"
    
    // Severe risk; immediate action.
    Critical Severity = "CRITICAL"

)

func (t *Trivy) Scan(
    ctx context.Context,
    severity Severity,
) (string, error) {
    // ...

Python

@dagger.enum_type
class Severity(dagger.Enum):
    """Vulnerability severity levels"""

    UNKNOWN = "UNKNOWN", "Undetermined risk; analyze further"
    LOW = "LOW", "Minimal risk; routine fix"
    MEDIUM = "MEDIUM", "Moderate risk; timely fix"
    HIGH = "HIGH", "Serious risk; quick fix needed."
    CRITICAL = "CRITICAL", "Severe risk; immediate action."

@dagger.object_type
class Trivy:
    @dagger.function
    async def scan(self, severity: Severity) -> str:
        ...

TypeScript

/**
 * Vulnerability severity levels
 */
@enumType()
class Severity {
    /**
     * Undetermined risk; analyze further.
     */
    static readonly Unknown: string = "UNKNOWN"
        
    /**
     * Minimal risk; routine fix.
     */
    static readonly Low: string = "LOW"
        
    /**
     * Moderate risk; timely fix.
     */
    static readonly Medium: string = "MEDIUM"
    
    /**
     * Serious risk; quick fix needed.
     */
    static readonly High: string = "HIGH"
    
    /**
     * Severe risk; immediate action.
     */
    static readonly Critical: string = "CRITICAL"
)

@object()
export class Trivy {
  @func()
  scan(severity: Severity): string {
    // ...
  }
}

Check out all the code here.

API Changes

Container.stdout, Container.stderr: ignore defaultArgs

On containers with no prior calls to withExec, Container.stdout or Container.stderr will now return an error, instead of implicitly calling withExec with no arguments which in turn would execute defaultArgs (equivalent of CMD in Dockerfiles).

The old behavior caused confusion, especially for containers created from an OCI image configured to run a long-running service.

Check out the code here.

Return absolute path on export instead of boolean

The core functions Container.export, Directory.export, or File.export returned a boolean value as the result of the operation. This was misleading because the boolean was never *false* as an unsuccessful export returns an error instead.

Now these functions return a more useful string with the absolute path on the host where the files were saved. It’s a possible breaking change for users that may be mistakenly checking for the boolean in code:

// ✅ ok, non-breaking!
_, err := ctr.Export(ctx)

// ❌ breaking
ok, err := ctr.Export(ctx)
if !ok { 
    // never executed

Check out the code here.

Remove deprecated schema fields

A number of deprecated schema fields have now been removed. We don’t expect most users to be affected by this - these fields have been deprecated for several months - but in case you are, the code change in this pull request has all the details.

Harmonize Container.withNewFile and Directory.withNewFile

Back in v0.4.1 we changed Directory.withNewFile to make contents a required argument. This helped reduce the verbosity of our Go SDK for common operations. However, we omitted to change Container.withNewFile in the same way, which created an inconsistency. We are correcting that inconsistency.

// ❌ before
ctr.
    WithNewFile("/foo", ContainerWithNewFileOpts{
        Contents: "some-content",
    }).
    WithNewFile("/bar")

// ✅ after
ctr.
    WithNewFile("/foo", "some-content").
    WithNewFile("/bar", ""

This a breaking change for the Go SDK, and TypeScript as well:

// ❌ before
ctr
    .withNewFile("/foo", { contents: "some-content" })
    .withNewFile("/bar")

// ✅ after
ctr
    .WithNewFile("/foo", "some-content")
    .WithNewFile("/bar", "")

It’s not breaking in Python because required arguments aren’t forced to be positional, but that could change in the future so it’s recommended to make the change anyway:

# ✅ before (non-breaking)
ctr
    .with_new_file("/foo", contents="some-content")
    .with_new_file("/bar")

# ✅ after
ctr
    .with_new_file("/foo", "some-content")
    .with_new_file("/bar", "")

See code here.

Ignore OCI entrypoint by default

Following up on user feedback, the implicit behavior of Container.withExec using the OCI image entrypoint by default caused frequent confusion and frustration, especially when the withExec is far removed from where the entrypoint is set.

To encourage more visibility, that default has been flipped. withExec won’t use the entrypoint unless explicitly expressed with the option useEntrypoint:

Go

// ❌ before
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}, ContainerWithExecOpts{
        SkipEntrypoint: true,
    }).
    WithExec([]string{"init"})

// ✅ after
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}).
    WithExec([]string{"init"}, ContainerWithExecOpts{
        UseEntrypoint: true

Python

//  before
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"], skip_entrypoint=True)
    .with_exec(["init"])
)

//  after
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"])
    .with_exec(["init"], use_entrypoint=True)
)

TypeScript

// ❌ before
ctr = dag.container().build(src)
  .withExec(["git", "init",{ skipEntrypoint: true })
  .withExec(["init")

// ✅ after
ctr = dag.container().build(src)
  .withExec(["git", "init"])
  .withExec(["init"], { useEntrypoint: true })

We recommend avoiding the use of OCI entrypoints, unless you need to for compatibility with external systems. This will keep your code more readable:

Go

// 🚀 better
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}).
    WithExec([]string{"dagger", "init"

Python

// 🚀 better
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"])
    .with_exec(["dagger", "init"])
)

TypeScript

// 🚀 better
ctr = dag.container().build(src)
  .withExec(["git", "init"])
  .withExec(["dagger", "init"])

Entrypoints are a relic of an era where the container engine was not fully programmable: they provide a rudimentary abstraction which becomes obsolete when you can create any abstraction you need, directly in your code here.

Don’t return Void

Note: this is only a breaking change in the Go SDK.

Dagger uses a special Void scalar as the return type for functions that don’t return anything or return only an error:

func (m *SomeModule) DoSomething() error {
    // do something that may return an error

When used as a dependency, the generated client binding returned a useless empty string (""):

// ❌ before
_, err := dag.SomeModule().DoSomething(ctx

Now SDKs simply don’t return the empty value:

// ✅ after
err := dag.SomeModule().DoSomething(ctx

This is a breaking change only in the Go SDK because Go handles errors like values instead of raising exceptions.

Check out the code here.

Deprecations

Use @func instead of @field - TypeScript

If you are using the TypeScript module runtime and still have the @field annotation in your code, consider updating to @func so that you will avoid being forced to update it when we remove it in a future version. This pull request has more details: https://github.com/dagger/dagger/pull/7701

What’s Next?

Thanks to our amazing community of Daggernauts, we are drinking from a firehose of feedback, bug reports and feature requests. As a result we are busier than ever developing the next wave of improvements to Dagger. By the way, we are looking for a Senior Software Engineer to join our team!

Here is a preview of some of the improvements we are working on.

If you have requests or feedback, or want to contribute, don’t hesitate to join our Discord server and say hello. And starring our GitHub repository is always appreciated!

Thank you for your support, we look forward to Daggerizing many more pipelines with you!

The Dagger team

Today we are introducing version 0.12 of the Dagger Engine. After several months of shipping bold new features at a breakneck pace (Functions, Daggerverse, Traces) for this release we focused on usability, polish, and quality. This includes a delightful interactive debugger; a more polished terminal and web UI; improved reliability and performance; a backwards compatibility system to make upgrades easier; better support for corporate networks; the ability to host modules on any git server; and much more.

Let’s dive in!

Interactive Debugging

For as long as we can remember, Dagger users wanted to have an interactive shell when their pipeline failed, with all the context at the point of failure. This is similar to a debugger experience, but without needing to set breakpoints explicitly. Other users wanted explicit breakpoints, and the same interactive experience. All these new capabilities are an enrichment to the Terminal type which was first introduced in Dagger v0.10.

As of Dagger v0.12, running dagger call with the –interactive (-i for short) flag will open a terminal in the context of a pipeline failure. No changes to your pipeline code required.

As an alternative to this convenient behavior, Terminal() can now be added anywhere in a pipeline and it will behave like a debugger breakpoint:

Go

func (m *Interactive) Container(ctx context.Context) (string, error) {
    return dag.Container().
        From("alpine:latest").
        Terminal().
        WithExec([]string{"sh", "-c", "echo hello world > /foo && cat /foo"}).
        Terminal().
        Stdout(ctx

Python

@function
async def container(self) -> str:
    return await (
        dag.container()
        .from_("alpine:latest")
        .terminal()
        .with_exec(["sh", "-c", "echo hello world > /foo && cat /foo"])
        .terminal()
        .stdout()
    )

TypeScript

@func()
async container(): Promise<string> {
  return dag
    .container()
    .from("alpine:latest")
    .terminal()
    .withExec(["sh", "-c", "echo hello world > /foo && cat /foo"])
    .terminal()
    .stdout()
}

Properties of this terminal can be customized, including reusing all the layers in a different container base image (e.g. Ubuntu instead of Alpine in the example below):

Go

func (m *Interactive) Dir(ctx context.Context) (string, error) {
    return dag.
        Git("https://github.com/dagger/dagger.git").
        Head().
        Tree().
        Terminal(dagger.DirectoryTerminalOpts{
            Container: dag.Container().From("ubuntu"),
            Cmd:       []string{"bash"},
        }).
        Directory().
        File("README.md").
        Contents(ctx

Python

@function
async def dir(self) -> str:
    return await (
        dag
        .git("https://github.com/dagger/dagger.git")
        .head()
        .tree()
        .terminal(
            container=dag.container().from_("ubuntu"),
            cmd=["bash"]
         )
        .directory(".")
        .file("README.md")
        .contents()
    )

TypeScript

@func()
async dir(): Promise<string> {
  return dag
    .git("https://github.com/dagger/dagger.git")
    .head()
    .tree()
    .terminal({container: dag.container().from("ubuntu"), cmd: ["bash"]})
    .directory(".")
    .file("README.md")
    .contents()
}

For a longer demonstration of how this works in practice, check out this Community Call Demo.

If you are interested in the code changes, check out these pull requests:

UI Polish

One of Dagger’s most popular features is the ability to visualize your pipeline execution in a detailed trace view, both in the terminal, and in a web browser.

This release includes many improvements to both our terminal and web user interfaces

Terminal UI: faster, now interactive

The Terminal UI is now interactive, while still presenting a svelte interface that gets out of your way. You can now use your keyboard and mouse to navigate, zoom in to spans, switch to a web view, and adjust the verbosity at runtime. A keymap is now shown along the bottom that doubles as a divider between the trace tree and the logs of the currently-zoomed span, saving precious screen space.

Additionally, the TUI has seen massive performance improvements. Previously viewing a large trace would burn a ton of CPU simply rendering the TUI and cropping it to fit the viewport. Now the TUI will only render the visible region, on top of many other optimizations.

To see a demonstration of this new feature, check out the demo below:

If you are interested in the code changes, see this pull request: https://github.com/dagger/dagger/pull/7671

Web UI: local view, CI view, flamecharts, async…

A few months ago, we announced Dagger Traces, a web counterpart to our popular terminal UI, to make it even easier to debug and monitor your Dagger pipelines. Since the initial release, we have added numerous improvements:

  • Local view shows all traces from your team’s local dev environments. This is a great way to collaborate with your team, and catch problems early, before changes are even pushed or processed by the CI server

  • CI view shows all traces from your CI environment, grouped by branch and commit, and annotated with useful git metadata

  • Traces include a new flame chart view, which enables clicking on points of interest from the top of the trace. This is especially useful to navigate large and complex traces

  • Better handling of asynchronous operations. Because of Dagger’s lazy execution model, many function calls are asynchronous: the API call to schedule it completes right away, but the actual operation will be executed later. This can make it difficult to connect “cause” from “effect” when viewing a complex trace. To improve readability, we now show “effects” alongside their “cause” in the trace, which is more intuitive.

  • We have made trace views faster overall, with even more performance improvements planned

  • General UI streamlining; less confusing lines, more noticeable focus indicator, less noise

  • Fixed various paper cuts: text selection issues, better handling of timeouts, removed confusing git metadata from local traces

You can watch a demo of all the recent improvements & additions here:

Corporate Network Support

Many users run their Dagger engines in corporate environments or other specialized networks that require use of custom certificate authorities or HTTP proxies.

The engine now supports configuring these proxies and custom CAs. This applies to the engine’s built-in operations (pulling container images, git repos, etc.), user containers created by `withExec` APIs and Module Function containers.

This support unblocks users with these specialized needs who previously could often not use Dagger at all. In particular, the support for automatically applying these settings to user containers and Module Functions means that users get this support “automagically” in their pipelines without needing to customize their Dagger code or any external Dagger module dependencies.

Further improvements to these features are planned, such as improved support for application specific proxy settings and configuration sourced from client machines.

You can learn more about how to configure these settings in our docs.

Reliability and Performance Improvements

We have made Dagger faster and more reliable for large and complex pipelines.

We have done this by fixing a long tail of issues in complex production pipelines, including the ones we use to develop Dagger itself. These issues are often difficult to reproduce, and typically involve  64-192 vCPU hosts, heavy-duty caching, pipeline parallelism, and highly diverse workloads. It’s painstaking work, but each production issue fixed makes the platform more robust for everyone. Thank you to our operators community for their invaluable feedback and collaboration! If you are running Dagger in production and have questions or feedback, we would love to hear from you on our Discord server.

We have been shipping these improvements gradually, in patch releases, typically a few releases per month. If you have been updating frequently, this may have gone unnoticed since the improvements were incremental, usually in the core of Dagger Engine, which is not something that many users notice in our changelog. Here is a more detailed list of relevant changes:

Compatibility Mode

We are introducing Compatibility Mode, a new feature that improves backwards compatibility of the Dagger Engine by simulating the behavior of older engines. This allows you to upgrade to the latest version of Dagger with confidence, knowing that your dependencies will still work, even if they target older versions.

At Dagger we believe that backwards compatibility is a crucial component of a successful software ecosystem. This is especially true for a young and fast-moving platform like Dagger: at our current rate of improvement, API changes cannot be entirely avoided. Compatibility Mode will allow us to improve backwards compatibility over time, without sacrificing the velocity which makes the Dagger community so special.

Note that Compatibility Mode still has limitations. Most importantly, we don’t guarantee 100% backwards compatibility, although we aim to get there eventually. We also don’t provide feature granularity: modules can’t pick and choose which API changes they upgrade, and which they don’t. We plan on adding this feature later, when the scale and maturity of the platform justifies it.

If you are interested in the code changes, check out these pull requests:

Modules on any Git server

We have removed a limitation which made it less practical to host Dagger Modules on some Git servers. Now, all Git servers are supported equally. This includes GitHub, GitLab, Bitbucket, and any other managed or self-hosted Git server.

Here is an example of calling the familiar “hello” module, hosted on Codeberg by a Dagger team member:

dagger -m codeberg.org/gerhard/daggerverse/hello call hello

One of the benefits of Dagger is to free your CI/CD pipelines from excessive platform lock-in, whenever possible. This improvement gets us one step closer to achieving this goal.

If you are interested in the code changes, see this pull request: https://github.com/dagger/dagger/pull/7511 

Enum support

By popular demand, Dagger SDKs now support custom enumeration types. This is useful for restricting possible values for an argument.

Go

// Vulnerability severity levels
type Severity string

const (
    // Undetermined risk; analyze further.
    Unknown Severity = "UNKNOWN"
        
    // Minimal risk; routine fix.
    Low Severity = "LOW"
        
    // Moderate risk; timely fix.
    Medium Severity = "MEDIUM"
    
    // Serious risk; quick fix needed.
    High Severity = "HIGH"
    
    // Severe risk; immediate action.
    Critical Severity = "CRITICAL"

)

func (t *Trivy) Scan(
    ctx context.Context,
    severity Severity,
) (string, error) {
    // ...

Python

@dagger.enum_type
class Severity(dagger.Enum):
    """Vulnerability severity levels"""

    UNKNOWN = "UNKNOWN", "Undetermined risk; analyze further"
    LOW = "LOW", "Minimal risk; routine fix"
    MEDIUM = "MEDIUM", "Moderate risk; timely fix"
    HIGH = "HIGH", "Serious risk; quick fix needed."
    CRITICAL = "CRITICAL", "Severe risk; immediate action."

@dagger.object_type
class Trivy:
    @dagger.function
    async def scan(self, severity: Severity) -> str:
        ...

TypeScript

/**
 * Vulnerability severity levels
 */
@enumType()
class Severity {
    /**
     * Undetermined risk; analyze further.
     */
    static readonly Unknown: string = "UNKNOWN"
        
    /**
     * Minimal risk; routine fix.
     */
    static readonly Low: string = "LOW"
        
    /**
     * Moderate risk; timely fix.
     */
    static readonly Medium: string = "MEDIUM"
    
    /**
     * Serious risk; quick fix needed.
     */
    static readonly High: string = "HIGH"
    
    /**
     * Severe risk; immediate action.
     */
    static readonly Critical: string = "CRITICAL"
)

@object()
export class Trivy {
  @func()
  scan(severity: Severity): string {
    // ...
  }
}

Check out all the code here.

API Changes

Container.stdout, Container.stderr: ignore defaultArgs

On containers with no prior calls to withExec, Container.stdout or Container.stderr will now return an error, instead of implicitly calling withExec with no arguments which in turn would execute defaultArgs (equivalent of CMD in Dockerfiles).

The old behavior caused confusion, especially for containers created from an OCI image configured to run a long-running service.

Check out the code here.

Return absolute path on export instead of boolean

The core functions Container.export, Directory.export, or File.export returned a boolean value as the result of the operation. This was misleading because the boolean was never *false* as an unsuccessful export returns an error instead.

Now these functions return a more useful string with the absolute path on the host where the files were saved. It’s a possible breaking change for users that may be mistakenly checking for the boolean in code:

// ✅ ok, non-breaking!
_, err := ctr.Export(ctx)

// ❌ breaking
ok, err := ctr.Export(ctx)
if !ok { 
    // never executed

Check out the code here.

Remove deprecated schema fields

A number of deprecated schema fields have now been removed. We don’t expect most users to be affected by this - these fields have been deprecated for several months - but in case you are, the code change in this pull request has all the details.

Harmonize Container.withNewFile and Directory.withNewFile

Back in v0.4.1 we changed Directory.withNewFile to make contents a required argument. This helped reduce the verbosity of our Go SDK for common operations. However, we omitted to change Container.withNewFile in the same way, which created an inconsistency. We are correcting that inconsistency.

// ❌ before
ctr.
    WithNewFile("/foo", ContainerWithNewFileOpts{
        Contents: "some-content",
    }).
    WithNewFile("/bar")

// ✅ after
ctr.
    WithNewFile("/foo", "some-content").
    WithNewFile("/bar", ""

This a breaking change for the Go SDK, and TypeScript as well:

// ❌ before
ctr
    .withNewFile("/foo", { contents: "some-content" })
    .withNewFile("/bar")

// ✅ after
ctr
    .WithNewFile("/foo", "some-content")
    .WithNewFile("/bar", "")

It’s not breaking in Python because required arguments aren’t forced to be positional, but that could change in the future so it’s recommended to make the change anyway:

# ✅ before (non-breaking)
ctr
    .with_new_file("/foo", contents="some-content")
    .with_new_file("/bar")

# ✅ after
ctr
    .with_new_file("/foo", "some-content")
    .with_new_file("/bar", "")

See code here.

Ignore OCI entrypoint by default

Following up on user feedback, the implicit behavior of Container.withExec using the OCI image entrypoint by default caused frequent confusion and frustration, especially when the withExec is far removed from where the entrypoint is set.

To encourage more visibility, that default has been flipped. withExec won’t use the entrypoint unless explicitly expressed with the option useEntrypoint:

Go

// ❌ before
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}, ContainerWithExecOpts{
        SkipEntrypoint: true,
    }).
    WithExec([]string{"init"})

// ✅ after
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}).
    WithExec([]string{"init"}, ContainerWithExecOpts{
        UseEntrypoint: true

Python

//  before
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"], skip_entrypoint=True)
    .with_exec(["init"])
)

//  after
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"])
    .with_exec(["init"], use_entrypoint=True)
)

TypeScript

// ❌ before
ctr = dag.container().build(src)
  .withExec(["git", "init",{ skipEntrypoint: true })
  .withExec(["init")

// ✅ after
ctr = dag.container().build(src)
  .withExec(["git", "init"])
  .withExec(["init"], { useEntrypoint: true })

We recommend avoiding the use of OCI entrypoints, unless you need to for compatibility with external systems. This will keep your code more readable:

Go

// 🚀 better
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}).
    WithExec([]string{"dagger", "init"

Python

// 🚀 better
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"])
    .with_exec(["dagger", "init"])
)

TypeScript

// 🚀 better
ctr = dag.container().build(src)
  .withExec(["git", "init"])
  .withExec(["dagger", "init"])

Entrypoints are a relic of an era where the container engine was not fully programmable: they provide a rudimentary abstraction which becomes obsolete when you can create any abstraction you need, directly in your code here.

Don’t return Void

Note: this is only a breaking change in the Go SDK.

Dagger uses a special Void scalar as the return type for functions that don’t return anything or return only an error:

func (m *SomeModule) DoSomething() error {
    // do something that may return an error

When used as a dependency, the generated client binding returned a useless empty string (""):

// ❌ before
_, err := dag.SomeModule().DoSomething(ctx

Now SDKs simply don’t return the empty value:

// ✅ after
err := dag.SomeModule().DoSomething(ctx

This is a breaking change only in the Go SDK because Go handles errors like values instead of raising exceptions.

Check out the code here.

Deprecations

Use @func instead of @field - TypeScript

If you are using the TypeScript module runtime and still have the @field annotation in your code, consider updating to @func so that you will avoid being forced to update it when we remove it in a future version. This pull request has more details: https://github.com/dagger/dagger/pull/7701

What’s Next?

Thanks to our amazing community of Daggernauts, we are drinking from a firehose of feedback, bug reports and feature requests. As a result we are busier than ever developing the next wave of improvements to Dagger. By the way, we are looking for a Senior Software Engineer to join our team!

Here is a preview of some of the improvements we are working on.

If you have requests or feedback, or want to contribute, don’t hesitate to join our Discord server and say hello. And starring our GitHub repository is always appreciated!

Thank you for your support, we look forward to Daggerizing many more pipelines with you!

The Dagger team

Today we are introducing version 0.12 of the Dagger Engine. After several months of shipping bold new features at a breakneck pace (Functions, Daggerverse, Traces) for this release we focused on usability, polish, and quality. This includes a delightful interactive debugger; a more polished terminal and web UI; improved reliability and performance; a backwards compatibility system to make upgrades easier; better support for corporate networks; the ability to host modules on any git server; and much more.

Let’s dive in!

Interactive Debugging

For as long as we can remember, Dagger users wanted to have an interactive shell when their pipeline failed, with all the context at the point of failure. This is similar to a debugger experience, but without needing to set breakpoints explicitly. Other users wanted explicit breakpoints, and the same interactive experience. All these new capabilities are an enrichment to the Terminal type which was first introduced in Dagger v0.10.

As of Dagger v0.12, running dagger call with the –interactive (-i for short) flag will open a terminal in the context of a pipeline failure. No changes to your pipeline code required.

As an alternative to this convenient behavior, Terminal() can now be added anywhere in a pipeline and it will behave like a debugger breakpoint:

Go

func (m *Interactive) Container(ctx context.Context) (string, error) {
    return dag.Container().
        From("alpine:latest").
        Terminal().
        WithExec([]string{"sh", "-c", "echo hello world > /foo && cat /foo"}).
        Terminal().
        Stdout(ctx

Python

@function
async def container(self) -> str:
    return await (
        dag.container()
        .from_("alpine:latest")
        .terminal()
        .with_exec(["sh", "-c", "echo hello world > /foo && cat /foo"])
        .terminal()
        .stdout()
    )

TypeScript

@func()
async container(): Promise<string> {
  return dag
    .container()
    .from("alpine:latest")
    .terminal()
    .withExec(["sh", "-c", "echo hello world > /foo && cat /foo"])
    .terminal()
    .stdout()
}

Properties of this terminal can be customized, including reusing all the layers in a different container base image (e.g. Ubuntu instead of Alpine in the example below):

Go

func (m *Interactive) Dir(ctx context.Context) (string, error) {
    return dag.
        Git("https://github.com/dagger/dagger.git").
        Head().
        Tree().
        Terminal(dagger.DirectoryTerminalOpts{
            Container: dag.Container().From("ubuntu"),
            Cmd:       []string{"bash"},
        }).
        Directory().
        File("README.md").
        Contents(ctx

Python

@function
async def dir(self) -> str:
    return await (
        dag
        .git("https://github.com/dagger/dagger.git")
        .head()
        .tree()
        .terminal(
            container=dag.container().from_("ubuntu"),
            cmd=["bash"]
         )
        .directory(".")
        .file("README.md")
        .contents()
    )

TypeScript

@func()
async dir(): Promise<string> {
  return dag
    .git("https://github.com/dagger/dagger.git")
    .head()
    .tree()
    .terminal({container: dag.container().from("ubuntu"), cmd: ["bash"]})
    .directory(".")
    .file("README.md")
    .contents()
}

For a longer demonstration of how this works in practice, check out this Community Call Demo.

If you are interested in the code changes, check out these pull requests:

UI Polish

One of Dagger’s most popular features is the ability to visualize your pipeline execution in a detailed trace view, both in the terminal, and in a web browser.

This release includes many improvements to both our terminal and web user interfaces

Terminal UI: faster, now interactive

The Terminal UI is now interactive, while still presenting a svelte interface that gets out of your way. You can now use your keyboard and mouse to navigate, zoom in to spans, switch to a web view, and adjust the verbosity at runtime. A keymap is now shown along the bottom that doubles as a divider between the trace tree and the logs of the currently-zoomed span, saving precious screen space.

Additionally, the TUI has seen massive performance improvements. Previously viewing a large trace would burn a ton of CPU simply rendering the TUI and cropping it to fit the viewport. Now the TUI will only render the visible region, on top of many other optimizations.

To see a demonstration of this new feature, check out the demo below:

If you are interested in the code changes, see this pull request: https://github.com/dagger/dagger/pull/7671

Web UI: local view, CI view, flamecharts, async…

A few months ago, we announced Dagger Traces, a web counterpart to our popular terminal UI, to make it even easier to debug and monitor your Dagger pipelines. Since the initial release, we have added numerous improvements:

  • Local view shows all traces from your team’s local dev environments. This is a great way to collaborate with your team, and catch problems early, before changes are even pushed or processed by the CI server

  • CI view shows all traces from your CI environment, grouped by branch and commit, and annotated with useful git metadata

  • Traces include a new flame chart view, which enables clicking on points of interest from the top of the trace. This is especially useful to navigate large and complex traces

  • Better handling of asynchronous operations. Because of Dagger’s lazy execution model, many function calls are asynchronous: the API call to schedule it completes right away, but the actual operation will be executed later. This can make it difficult to connect “cause” from “effect” when viewing a complex trace. To improve readability, we now show “effects” alongside their “cause” in the trace, which is more intuitive.

  • We have made trace views faster overall, with even more performance improvements planned

  • General UI streamlining; less confusing lines, more noticeable focus indicator, less noise

  • Fixed various paper cuts: text selection issues, better handling of timeouts, removed confusing git metadata from local traces

You can watch a demo of all the recent improvements & additions here:

Corporate Network Support

Many users run their Dagger engines in corporate environments or other specialized networks that require use of custom certificate authorities or HTTP proxies.

The engine now supports configuring these proxies and custom CAs. This applies to the engine’s built-in operations (pulling container images, git repos, etc.), user containers created by `withExec` APIs and Module Function containers.

This support unblocks users with these specialized needs who previously could often not use Dagger at all. In particular, the support for automatically applying these settings to user containers and Module Functions means that users get this support “automagically” in their pipelines without needing to customize their Dagger code or any external Dagger module dependencies.

Further improvements to these features are planned, such as improved support for application specific proxy settings and configuration sourced from client machines.

You can learn more about how to configure these settings in our docs.

Reliability and Performance Improvements

We have made Dagger faster and more reliable for large and complex pipelines.

We have done this by fixing a long tail of issues in complex production pipelines, including the ones we use to develop Dagger itself. These issues are often difficult to reproduce, and typically involve  64-192 vCPU hosts, heavy-duty caching, pipeline parallelism, and highly diverse workloads. It’s painstaking work, but each production issue fixed makes the platform more robust for everyone. Thank you to our operators community for their invaluable feedback and collaboration! If you are running Dagger in production and have questions or feedback, we would love to hear from you on our Discord server.

We have been shipping these improvements gradually, in patch releases, typically a few releases per month. If you have been updating frequently, this may have gone unnoticed since the improvements were incremental, usually in the core of Dagger Engine, which is not something that many users notice in our changelog. Here is a more detailed list of relevant changes:

Compatibility Mode

We are introducing Compatibility Mode, a new feature that improves backwards compatibility of the Dagger Engine by simulating the behavior of older engines. This allows you to upgrade to the latest version of Dagger with confidence, knowing that your dependencies will still work, even if they target older versions.

At Dagger we believe that backwards compatibility is a crucial component of a successful software ecosystem. This is especially true for a young and fast-moving platform like Dagger: at our current rate of improvement, API changes cannot be entirely avoided. Compatibility Mode will allow us to improve backwards compatibility over time, without sacrificing the velocity which makes the Dagger community so special.

Note that Compatibility Mode still has limitations. Most importantly, we don’t guarantee 100% backwards compatibility, although we aim to get there eventually. We also don’t provide feature granularity: modules can’t pick and choose which API changes they upgrade, and which they don’t. We plan on adding this feature later, when the scale and maturity of the platform justifies it.

If you are interested in the code changes, check out these pull requests:

Modules on any Git server

We have removed a limitation which made it less practical to host Dagger Modules on some Git servers. Now, all Git servers are supported equally. This includes GitHub, GitLab, Bitbucket, and any other managed or self-hosted Git server.

Here is an example of calling the familiar “hello” module, hosted on Codeberg by a Dagger team member:

dagger -m codeberg.org/gerhard/daggerverse/hello call hello

One of the benefits of Dagger is to free your CI/CD pipelines from excessive platform lock-in, whenever possible. This improvement gets us one step closer to achieving this goal.

If you are interested in the code changes, see this pull request: https://github.com/dagger/dagger/pull/7511 

Enum support

By popular demand, Dagger SDKs now support custom enumeration types. This is useful for restricting possible values for an argument.

Go

// Vulnerability severity levels
type Severity string

const (
    // Undetermined risk; analyze further.
    Unknown Severity = "UNKNOWN"
        
    // Minimal risk; routine fix.
    Low Severity = "LOW"
        
    // Moderate risk; timely fix.
    Medium Severity = "MEDIUM"
    
    // Serious risk; quick fix needed.
    High Severity = "HIGH"
    
    // Severe risk; immediate action.
    Critical Severity = "CRITICAL"

)

func (t *Trivy) Scan(
    ctx context.Context,
    severity Severity,
) (string, error) {
    // ...

Python

@dagger.enum_type
class Severity(dagger.Enum):
    """Vulnerability severity levels"""

    UNKNOWN = "UNKNOWN", "Undetermined risk; analyze further"
    LOW = "LOW", "Minimal risk; routine fix"
    MEDIUM = "MEDIUM", "Moderate risk; timely fix"
    HIGH = "HIGH", "Serious risk; quick fix needed."
    CRITICAL = "CRITICAL", "Severe risk; immediate action."

@dagger.object_type
class Trivy:
    @dagger.function
    async def scan(self, severity: Severity) -> str:
        ...

TypeScript

/**
 * Vulnerability severity levels
 */
@enumType()
class Severity {
    /**
     * Undetermined risk; analyze further.
     */
    static readonly Unknown: string = "UNKNOWN"
        
    /**
     * Minimal risk; routine fix.
     */
    static readonly Low: string = "LOW"
        
    /**
     * Moderate risk; timely fix.
     */
    static readonly Medium: string = "MEDIUM"
    
    /**
     * Serious risk; quick fix needed.
     */
    static readonly High: string = "HIGH"
    
    /**
     * Severe risk; immediate action.
     */
    static readonly Critical: string = "CRITICAL"
)

@object()
export class Trivy {
  @func()
  scan(severity: Severity): string {
    // ...
  }
}

Check out all the code here.

API Changes

Container.stdout, Container.stderr: ignore defaultArgs

On containers with no prior calls to withExec, Container.stdout or Container.stderr will now return an error, instead of implicitly calling withExec with no arguments which in turn would execute defaultArgs (equivalent of CMD in Dockerfiles).

The old behavior caused confusion, especially for containers created from an OCI image configured to run a long-running service.

Check out the code here.

Return absolute path on export instead of boolean

The core functions Container.export, Directory.export, or File.export returned a boolean value as the result of the operation. This was misleading because the boolean was never *false* as an unsuccessful export returns an error instead.

Now these functions return a more useful string with the absolute path on the host where the files were saved. It’s a possible breaking change for users that may be mistakenly checking for the boolean in code:

// ✅ ok, non-breaking!
_, err := ctr.Export(ctx)

// ❌ breaking
ok, err := ctr.Export(ctx)
if !ok { 
    // never executed

Check out the code here.

Remove deprecated schema fields

A number of deprecated schema fields have now been removed. We don’t expect most users to be affected by this - these fields have been deprecated for several months - but in case you are, the code change in this pull request has all the details.

Harmonize Container.withNewFile and Directory.withNewFile

Back in v0.4.1 we changed Directory.withNewFile to make contents a required argument. This helped reduce the verbosity of our Go SDK for common operations. However, we omitted to change Container.withNewFile in the same way, which created an inconsistency. We are correcting that inconsistency.

// ❌ before
ctr.
    WithNewFile("/foo", ContainerWithNewFileOpts{
        Contents: "some-content",
    }).
    WithNewFile("/bar")

// ✅ after
ctr.
    WithNewFile("/foo", "some-content").
    WithNewFile("/bar", ""

This a breaking change for the Go SDK, and TypeScript as well:

// ❌ before
ctr
    .withNewFile("/foo", { contents: "some-content" })
    .withNewFile("/bar")

// ✅ after
ctr
    .WithNewFile("/foo", "some-content")
    .WithNewFile("/bar", "")

It’s not breaking in Python because required arguments aren’t forced to be positional, but that could change in the future so it’s recommended to make the change anyway:

# ✅ before (non-breaking)
ctr
    .with_new_file("/foo", contents="some-content")
    .with_new_file("/bar")

# ✅ after
ctr
    .with_new_file("/foo", "some-content")
    .with_new_file("/bar", "")

See code here.

Ignore OCI entrypoint by default

Following up on user feedback, the implicit behavior of Container.withExec using the OCI image entrypoint by default caused frequent confusion and frustration, especially when the withExec is far removed from where the entrypoint is set.

To encourage more visibility, that default has been flipped. withExec won’t use the entrypoint unless explicitly expressed with the option useEntrypoint:

Go

// ❌ before
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}, ContainerWithExecOpts{
        SkipEntrypoint: true,
    }).
    WithExec([]string{"init"})

// ✅ after
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}).
    WithExec([]string{"init"}, ContainerWithExecOpts{
        UseEntrypoint: true

Python

//  before
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"], skip_entrypoint=True)
    .with_exec(["init"])
)

//  after
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"])
    .with_exec(["init"], use_entrypoint=True)
)

TypeScript

// ❌ before
ctr = dag.container().build(src)
  .withExec(["git", "init",{ skipEntrypoint: true })
  .withExec(["init")

// ✅ after
ctr = dag.container().build(src)
  .withExec(["git", "init"])
  .withExec(["init"], { useEntrypoint: true })

We recommend avoiding the use of OCI entrypoints, unless you need to for compatibility with external systems. This will keep your code more readable:

Go

// 🚀 better
ctr := dag.Container().Build(src).
    WithExec([]string{"git", "init"}).
    WithExec([]string{"dagger", "init"

Python

// 🚀 better
ctr = (
    dag.container().build(src)
    .with_exec(["git", "init"])
    .with_exec(["dagger", "init"])
)

TypeScript

// 🚀 better
ctr = dag.container().build(src)
  .withExec(["git", "init"])
  .withExec(["dagger", "init"])

Entrypoints are a relic of an era where the container engine was not fully programmable: they provide a rudimentary abstraction which becomes obsolete when you can create any abstraction you need, directly in your code here.

Don’t return Void

Note: this is only a breaking change in the Go SDK.

Dagger uses a special Void scalar as the return type for functions that don’t return anything or return only an error:

func (m *SomeModule) DoSomething() error {
    // do something that may return an error

When used as a dependency, the generated client binding returned a useless empty string (""):

// ❌ before
_, err := dag.SomeModule().DoSomething(ctx

Now SDKs simply don’t return the empty value:

// ✅ after
err := dag.SomeModule().DoSomething(ctx

This is a breaking change only in the Go SDK because Go handles errors like values instead of raising exceptions.

Check out the code here.

Deprecations

Use @func instead of @field - TypeScript

If you are using the TypeScript module runtime and still have the @field annotation in your code, consider updating to @func so that you will avoid being forced to update it when we remove it in a future version. This pull request has more details: https://github.com/dagger/dagger/pull/7701

What’s Next?

Thanks to our amazing community of Daggernauts, we are drinking from a firehose of feedback, bug reports and feature requests. As a result we are busier than ever developing the next wave of improvements to Dagger. By the way, we are looking for a Senior Software Engineer to join our team!

Here is a preview of some of the improvements we are working on.

If you have requests or feedback, or want to contribute, don’t hesitate to join our Discord server and say hello. And starring our GitHub repository is always appreciated!

Thank you for your support, we look forward to Daggerizing many more pipelines with you!

The Dagger team

Get Involved With the community

Discover what our community is doing, and join the conversation on Discord & GitHub to help shape the evolution of Dagger.

Subscribe to our newsletter

Get Involved With the community

Discover what our community is doing, and join the conversation on Discord & GitHub to help shape the evolution of Dagger.

Subscribe to our newsletter

Get Involved With the community

Discover what our community is doing, and join the conversation on Discord & GitHub to help shape the evolution of Dagger.

Subscribe to our newsletter