ruk·si

🗡️ Dagger

Updated at 2024-08-07 19:39

TL;DR:

  • Dagger is a bit like Make, but with Docker containers.
  • Dagger might be worth a look if you have a complex build or other pipelines.

The idea is nice that you wouldn't have to push to a remote repository to test your CI/CD pipeline. But it's not worth the overhead for simple projects, especially compared to Make or whatever you prefer.

Dagger is CI/CD engine that uses Docker containers to run pipeline steps. This allows you to run all steps locally in the same way as in the CI/CD environment with little overhead and setup.

```bash
dagger init --sdk=python
# and edit the dagger/src/main/__init__.py
import random

import dagger
from dagger import function, dag, object_type


@object_type
class MyDagger:

    @function
    async def publish(self, source: dagger.Directory) -> str:
        # tests, builds and publishes a container image of the application to a registry
        await self.test(source)
        return await self.build(source).publish(
            f"ttl.sh/myapp-{random.randrange(10 ** 8)}"
        )

    @function
    def build(self, source: dagger.Directory) -> dagger.Container:
        # performs a multi-stage build and returns a final container image
        build = (
            self.build_base(source)
            .with_exec(["npm", "run", "build"])
            .directory("./dist")
        )
        return (
            dag.container()
            .from_("nginx:1.25-alpine")
            .with_directory("/usr/share/nginx/html", build)
            .with_exposed_port(8080)
        )

    @function
    async def test(self, source: dagger.Directory) -> str:
        # runs the application's unit tests and returns the results
        return await (
            self.build_base(source)
            .with_exec(["npm", "run", "test:unit", "run"])
            .stdout()
        )

    @function
    def build_base(self, source: dagger.Directory) -> dagger.Container:
        # creates a container with the build environment for the application
        node_cache = dag.cache_volume("node")
        return (
            dag.container()
            .from_("node:21-slim")
            .with_directory("/src", source)
            .with_mounted_cache("/src/node_modules", node_cache)
            .with_workdir("/src")
            .with_exec(["npm", "install"])
        )

Each Dagger function runs in a separate container. This allows using different languages or version for each pipeline step.

# these call Dagger Functions from the Python definitions
dagger call build-base --source=. terminal --cmd=bash
dagger call test --source=.
dagger call build --source=.
dagger call publish --source=.

Dagger has very limited access to the host system. That is why you need to give the --source=. to each call.

Dagger can also be used to maintain a local development environment.

dagger call build --source=. as-service up --ports=8080:80

But this does have some downsides as discussed below.

Each Dagger function receives a separate copy of the source code. This is a blessing and a curse; nothing can affect what is running but also means if you change something, you need to rerun the whole Dagger function.

If you have a Dagger function that starts a development server; by default, the source code won't get updated if you change the code in your IDE.

CI/CD platform support is great. It's fairly straightforward to use Dagger through popular CI/CD services like GitHub Actions, GitLab CI/CD, or CircleCI.

name: dagger

on:
  push:
    branches: [ main ]

jobs:
  build:
    name: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Call Dagger Function
        uses: dagger/dagger-for-github@v5
        with:
          version: "latest"
          verb: call
          module: github.com/kpenfound/dagger-modules/golang@v0.2.0
          args: build --project=. --args=.