detached 00586f3

fix(push): split buildx --push into --load + docker push

BuildKit's --push uploads each layer as a single monolithic HTTP
request. For images >100MB this gets rejected by Cloudflare's
per-request size limit. Using --load to import into the local
daemon then docker push gives us chunked uploads (~5MB/request)
that stay well under the limit.
commit
00586f3d50a4d012a0e6972f4785803ab74d4320
author
brice · · 2026-02-06 13:02 UTC
parent
a26fdcc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +40 -10
M internal/adapters/in/cli/push.go +31 -10
@@ -127,7 +127,35 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
127127 return fmt.Errorf("no Dockerfile found in current directory")
128128 }
129129
130- fmt.Println("\nBuilding and pushing...")
130+ // Build and load into local daemon (NOT --push).
131+ // BuildKit's --push uses monolithic blob uploads that exceed
132+ // Cloudflare's 100MB per-request limit. Loading locally then
133+ // using docker push gives us chunked uploads (~5MB per request).
134+ fmt.Println("\nBuilding image...")
135+ buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, buildArgs, versionRef, latestRef)...) // #nosec G204
136+ buildCmd.Stdout = os.Stdout
137+ buildCmd.Stderr = os.Stderr
138+ if err := buildCmd.Run(); err != nil {
139+ return fmt.Errorf("docker buildx build failed: %w", err)
140+ }
141+
142+ fmt.Println("Pushing...")
143+ if err := dockerPush(ctx, latestRef); err != nil {
144+ return fmt.Errorf("failed to push %s: %w", latestRef, err)
145+ }
146+ if version != "latest" {
147+ if err := dockerPush(ctx, versionRef); err != nil {
148+ return fmt.Errorf("failed to push %s: %w", versionRef, err)
149+ }
150+ }
151+
152+ return nil
153+}
154+
155+// buildImageArgs constructs the docker buildx build arguments.
156+// Uses --load instead of --push so the image is loaded into the local
157+// daemon, allowing docker push to handle the upload with chunked requests.
158+func buildImageArgs(version, platform string, buildArgs []string, versionRef, latestRef string) []string {
131159 args := []string{
132160 "buildx", "build",
133161 "--platform", platform,
@@ -140,15 +168,8 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
140168 for _, ba := range buildArgs {
141169 args = append(args, "--build-arg", ba)
142170 }
143- args = append(args, "--push", ".")
144-
145- dockerCmd := exec.CommandContext(ctx, "docker", args...) // #nosec G204
146- dockerCmd.Stdout = os.Stdout
147- dockerCmd.Stderr = os.Stderr
148- if err := dockerCmd.Run(); err != nil {
149- return fmt.Errorf("docker buildx failed: %w", err)
150- }
151- return nil
171+ args = append(args, "--load", ".")
172+ return args
152173 }
153174
154175 func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {
@@ -127,7 +127,35 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
127 return fmt.Errorf("no Dockerfile found in current directory")127 return fmt.Errorf("no Dockerfile found in current directory")
128 }128 }
129 129
130- fmt.Println("\nBuilding and pushing...")130+ // Build and load into local daemon (NOT --push).
131+ // BuildKit's --push uses monolithic blob uploads that exceed
132+ // Cloudflare's 100MB per-request limit. Loading locally then
133+ // using docker push gives us chunked uploads (~5MB per request).
134+ fmt.Println("\nBuilding image...")
135+ buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, buildArgs, versionRef, latestRef)...) // #nosec G204
136+ buildCmd.Stdout = os.Stdout
137+ buildCmd.Stderr = os.Stderr
138+ if err := buildCmd.Run(); err != nil {
139+ return fmt.Errorf("docker buildx build failed: %w", err)
140+ }
141+
142+ fmt.Println("Pushing...")
143+ if err := dockerPush(ctx, latestRef); err != nil {
144+ return fmt.Errorf("failed to push %s: %w", latestRef, err)
145+ }
146+ if version != "latest" {
147+ if err := dockerPush(ctx, versionRef); err != nil {
148+ return fmt.Errorf("failed to push %s: %w", versionRef, err)
149+ }
150+ }
151+
152+ return nil
153+}
154+
155+// buildImageArgs constructs the docker buildx build arguments.
156+// Uses --load instead of --push so the image is loaded into the local
157+// daemon, allowing docker push to handle the upload with chunked requests.
158+func buildImageArgs(version, platform string, buildArgs []string, versionRef, latestRef string) []string {
131 args := []string{159 args := []string{
132 "buildx", "build",160 "buildx", "build",
133 "--platform", platform,161 "--platform", platform,
@@ -140,15 +168,8 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
140 for _, ba := range buildArgs {168 for _, ba := range buildArgs {
141 args = append(args, "--build-arg", ba)169 args = append(args, "--build-arg", ba)
142 }170 }
143- args = append(args, "--push", ".")171+ args = append(args, "--load", ".")
144-172+ return args
145- dockerCmd := exec.CommandContext(ctx, "docker", args...) // #nosec G204
146- dockerCmd.Stdout = os.Stdout
147- dockerCmd.Stderr = os.Stderr
148- if err := dockerCmd.Run(); err != nil {
149- return fmt.Errorf("docker buildx failed: %w", err)
150- }
151- return nil
152 }173 }
153 174
154 func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {175 func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {
M internal/adapters/in/cli/push_test.go +9 -0
@@ -60,3 +60,12 @@ func TestParseImageRef(t *testing.T) {
6060 })
6161 }
6262 }
63+
64+func TestBuildAndPush_BuildArgs(t *testing.T) {
65+ // Verify buildImageArgs produces --load instead of --push
66+ args := buildImageArgs("v1.0.0", "linux/amd64", []string{"CGO_ENABLED=0"}, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
67+
68+ assert.Contains(t, args, "--load")
69+ assert.NotContains(t, args, "--push")
70+ assert.Contains(t, args, "--platform")
71+}
@@ -60,3 +60,12 @@ func TestParseImageRef(t *testing.T) {
60 })60 })
61 }61 }
62 }62 }
63+
64+func TestBuildAndPush_BuildArgs(t *testing.T) {
65+ // Verify buildImageArgs produces --load instead of --push
66+ args := buildImageArgs("v1.0.0", "linux/amd64", []string{"CGO_ENABLED=0"}, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
67+
68+ assert.Contains(t, args, "--load")
69+ assert.NotContains(t, args, "--push")
70+ assert.Contains(t, args, "--platform")
71+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close