Dagger
Containerized magic with Go and BuildKit
Will put some experience notes here soon as I've successfully built angular and nginx containers with it and overall was a great experience. With the upcoming services support, I can see a whole lot more usage cases too.
Example Building An Angular Project
Using mage, here's an example of how to invoke Mage to build an angular project without any angular tooling installed locally.
const AngularVersion = "15"
// Build runs the angular build via Dagger.
func (Dagger) Build(ctx context.Context) error {
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
pterm.Error.Printfln("unable to connect to dagger: %s", err)
return err
}
defer client.Close()
homedir, err := os.UserHomeDir()
if err != nil {
return err
}
npm := client.Container().From("node:lts-alpine")
npm = npm.WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src")
path := "dist/"
npm = npm.WithExec([]string{"npm", "install", "-g", fmt.Sprintf("@angular/cli@%s", AngularVersion)})
npm = npm.WithExec([]string{"ng", "config", "-g", "cli.warnings.versionMismatch", "false"})
npm = npm.WithExec([]string{"ng", "v"})
npm = npm.WithExec([]string{"npm", "ci"})
npm = npm.WithExec([]string{"ng", "build", "--configuration", "production"})
// Copy "dist/" from container to host.
_, err = npm.Directory(path).Export(ctx, path)
if err != nil {
return err
}
return nil
}
example of handling both local and CI private npm auth
Here you can handle both running in a CI context or a remote context by evaluting for a CI variable that would provide back a CI system generated .npmrc
.
If this isn't provided, mount the file from the home directory into the build container.
Note this container isn't for publishing, it's a build container copying the dist/
contents back to the project directory.
npmrcFile := &dagger.Secret{}
// bypassing any mounting of npmrc, as CI tooling should update any private inline with current file here
if os.Getenv("NPM_CONFIG_USERCONFIG") != "" {
pterm.Info.Printfln("[OVERRIDE] NPM_CONFIG_USERCONFIG: %s", os.Getenv("NPM_CONFIG_USERCONFIG"))
npmrcDir := filepath.Dir(os.Getenv("NPM_CONFIG_USERCONFIG"))
} else {
// [DEFAULT] NPM config set from home/.npmrc
npmrcFile = client.Host().Directory(homedir, dagger.HostDirectoryOpts{Include: []string{".npmrc"}}).File(".npmrc").Secret()
// if npmrcFile doesn't exist output error
if _, err := os.Stat(filepath.Join(homedir, ".npmrc")); os.IsNotExist(err) {
return errors.New("missing npmrc file")
}
npm = npm.WithMountedSecret("/root/.npmrc", npmrcFile)
}
Building a Go App with Caching
Using Mage and the excellent chainguard go builder image, this shows building a binary for the current platform and architecture, while wrapping up the entire build process inside the Dagger engine.
The output goes to the standard .artifacts
directory I include in all my projects, which should be ignored by git.
package main
import (
"context"
"os"
"path/filepath"
"runtime"
"dagger.io/dagger"
"github.com/magefile/mage/mg"
"github.com/pterm/pterm"
)
// Build contains all the build related mage targets.
type Build mg.Namespace
const (
// ArtifactDirectory is a directory containing artifacts for the project and shouldn't be committed to source.
ArtifactDirectory = ".artifacts"
// PermissionUserReadWriteExecute is the permissions for the artifact directory.
PermissionUserReadWriteExecute = 0o0700
)
// TargetBuildDirectory is the directory where the build artifacts will be placed.
var TargetBuildDirectory = filepath.Join(ArtifactDirectory, "builds")
// 🔨 MyAppName builds the service using Dagger for the current system architecture.
//
// Development notes: This is a fully containerized build, using Dagger. Requires Docker.
func (Build) MyAppName() error {
ctx := context.Background()
pterm.DefaultHeader.Println("Building with Dagger")
buildThis := "./myApp/main.go" // this is the specific file to build, could be an input variable/slice though
appName := "myApp"
// make the target directory
if err := os.MkdirAll(filepath.Join(TargetBuildDirectory, appName), PermissionUserReadWriteExecute); err != nil {
return err
}
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
return err
}
defer client.Close()
// get reference to the local project
src := client.Host().Directory(".")
cachedBuild := client.CacheVolume("go-build-cache")
cachedMod := client.CacheVolume("go-mod-cache")
modcache := "/nonroot/.cache/go-mod-cache"
buildcache := "/nonroot/.cache/go-build-cache"
// get `golang` image
golang := client.Container().From("cgr.dev/chainguard/go:latest").
WithEnvVariable("CGO_ENABLED", "0").
WithEnvVariable("GOOS", runtime.GOOS).
WithEnvVariable("GOARCH", runtime.GOARCH).
WithEnvVariable("GOMODCACHE", modcache). // attempt to optimize mod and build caching
WithEnvVariable("GOCACHE", buildcache)
// mount cloned repository into `golang` image
golang = golang.WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithMountedCache(modcache, cachedMod).
WithMountedCache(buildcache, cachedBuild)
// define the application build command
outputDirectory := filepath.Join(TargetBuildDirectory, appName)
outputFile := filepath.Join(outputDirectory, fmt.Sprintf("%s-service",appName))
golang = golang.WithExec([]string{"build", "-o", outputFile, "-ldflags", "-s -w", "-trimpath", buildThis}) // NOTE: target is preset for now
// get reference to build output directory in container
output := golang.Directory(outputDirectory).File(fmt.Sprintf("%s-service",appName)) // this is the specific file rather than a mounted directory as we just want this one artifact
// write contents of container build/ directory to the host
_, err = output.Export(ctx, outputFile)
if err != nil {
return err
}
return nil
}