#Tag Local Image?

1 messages · Page 1 of 1 (latest)

cobalt ingot
#

I have a pipeline that produces multi-stage images, where image C is FROM B, B is FROM A, and all three A B & C should be usable in docker run. I can't see any way to assign a tag either before or after Directory.dockerBuild() a la docker build -t something .. Currently, I have something like

const runEnvironment_imageRef = await client
    .host()
    .directory(`RunEnvironment`)
    .dockerBuild()
    .withRegistryAuth(runEnvironment_imageUrl, configuration.containerRegistry_user, containerRegistry_password)
    .publish(runEnvironment_imageUrl)

const buildEnvironment_imageRef = await client
    .directory()
    .withDirectory("/", gitDirectory)
    .dockerBuild({
        dockerfile: `Dagger/BuildEnvironment/Dockerfile`,
        buildArgs: [{ name: `RunEnvironment_ImageRef`, value: runEnvironment_imageRef }]
    })
    .withRegistryAuth(buildEnvironment_imageUrl, configuration.containerRegistry_user, containerRegistry_password)
    .publish(buildEnvironment_imageUrl)

Build args using the output of publish are used to force a dependency between the two (avoid having the build environment build from an outdated run environment while the new run environment is still building). There are several problems with using the shared registry URL as this intermediate:

  • If there are problems with the container registry, my local testing is interrupted (running into this right now!).
  • I should be able to test this out locally without pushing anything to the shared registry.
  • One of these containers is necessarily big, and uploading can take a long time. But C shouldn't have to wait for B to finish uploading, before it begins building - the two operations should, ideally, occur simultaneously.
  • For each subsequent stage, Docker downloads the previous one from the registry, which it had just uploaded. This doesn't happen with docker build -t.

Using localhost or a plain tag name in publish doesn't work (they're treated as hostnames).

cobalt ingot
#

I should clarify, by "local" I mean local to the engine, not the SDK.

atomic sonnet
#

I've also run a local docker registry:2 and then hooked dagger up to that

cobalt ingot
#

Thanks for the suggestions.

I had come across that "Load Container Images" recipe, but I don't think it would actually help in this scenario because Dagger doesn't even have access to the host's local images anyways, and I don't think there's any way to plug a host's image file into the FROM of a container build that Dagger executes.

Running a local registry did occur to me, but it's a workaround rather than a proper fix. Dagger would still upload and then re-download the image, albeit faster. Disk space requirements are still increased unnecessarily.

And, upon further reflection, it seems like a bug to me that the download occurs even without a tag - when B build with FROM A, shouldn't we see something like this?

BuildKit: (finishes building A) Hey, server, here's A:latest
Server: Upload completed
BuildKit: (begins building B) Hey, server, I need A:latest
Server: A:latest is DEADBEEF123
BuildKit: I already have a layer with the hash DEADBEEF123 because I built it a few minutes ago and finished uploading it a few seconds ago, no need to proceed with the download.

cobalt ingot
#

I realized that perhaps I should put A, B, and C into a single Dockerfile, like

FROM foo as a
...

FROM a as b
...

FROM b as builder
...

FROM a as c
COPY --from=builder ...
...
...
// Build the final stage first
const c_container = await dockerContext_directory
    .dockerBuild()
    .sync()
// Build and publish earlier stages - they will be cached at this point
const [c_imageRef, b_imageRef, a_imageRef] = await Promise.all([
    c_container
        .withRegistryAuth(configuration.containerRegistry_url, configuration.containerRegistry_user, containerRegistry_password)
        .publish(`${configuration.containerRegistry_url}/c:latest`),

    dockerContext_directory
        .dockerBuild({ target: `b` })
        .withRegistryAuth(configuration.containerRegistry_url, configuration.containerRegistry_user, containerRegistry_password)
        .publish(`${configuration.containerRegistry_url}/b:latest`),

    dockerContext_directory
        .dockerBuild({ target: `a` })
        .withRegistryAuth(configuration.containerRegistry_url, configuration.containerRegistry_user, containerRegistry_password)
        .publish(`${configuration.containerRegistry_url}/a:latest`)])

Then, intermediate labels aren't necessary.

I have yet to see if dockerBuild({ target: "b" }) actually utilizes the previously cached layers, or if this will cause the B stages to run twice and the A stages to run three times. But I'm pessimistic about it, becuase my first run of this approach failed @ Promise.all due to a syntax error, and upon re-running the pipeline, despite having made no changes other than fixing the Dagger JS file (which is not included in dockerContext_directory), I'm seeing zero cache hits whatsoever. This is leading me to wonder if I shouldn't be using a Dockerfile in the first place, if that apprach is just not compatible with Dagger because they're both implementing the same concepts in different ways with different caches.

fervent token
#

Then, intermediate labels aren't necessary.

I was about to suggest this multi-stage approach since buildkit/dagger doesn't have the concept of a built image per-se. I think you're stumbling across the perfect use-case which Dagger was created for. Using your favorite SDK, you could do something like this:

func main() {
    ctx := context.Background()
    client, err := dagger.Connect(ctx)
    if err != nil {
        panic(err)
    }

    defer client.Close()

    c1 := client.Container().
        From("alpine").
        WithExec([]string{"echo", "lint"})

    c2 := c1.WithExec([]string{"echo", "build"})

    c3 := c2.WithExec([]string{"echo", "test"})

    c1.Publish(ctx, "ttl.sh/foo:lint")
    c2.Publish(ctx, "ttl.sh/foo:build")
    c3.Publish(ctx, "ttl.sh/foo:test")
}
#

I have yet to see if dockerBuild({ target: "b" }) actually utilizes the previously cached layers, or if this will cause the B stages to run twice and the A stages to run three times. But I'm pessimistic about it, becuase my first run of this approach failed @ Promise.all due to a syntax error, and upon re-running the pipeline, despite having made no changes other than fixing the Dagger JS file (which is not included in dockerContext_directory), I'm seeing zero cache hits whatsoever.

it should be re-using cache.. maybe you're doing something that invalides the cache

fervent token
cobalt ingot
#

Yeah, I ended up converting everything to pure dagger yesterday and it works a lot better. I did still run into a few unexpected cache busts (small change to a later stage caused earlier non-dependent stage to re-build), but that's another topic. Moreover, it's nice to not have to make a decision about what goes inside Dockerfile vs inside Dagger-file.