detached a338153

feat(cli): add custom Dockerfile flag to gordon push --build

commit
a33815347b0ba78801f91ca1af291e5a4b906235
author
brice · · 2026-02-06 15:42 UTC
committer
brice · · 2026-02-06 19:34 UTC
parent
c00c10c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +41 -17
M docs/cli/push.md +7 -2
@@ -21,6 +21,7 @@ gordon push <domain> [options]
2121 | Option | Description |
2222 |--------|-------------|
2323 | `--build` | Build the image first using `docker buildx` |
24+| `-f, --file` | Path to Dockerfile (default: `./Dockerfile`, used with `--build`) |
2425 | `--platform` | Target platform for buildx (default: `linux/amd64`) |
2526 | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) |
2627 | `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) |
@@ -37,8 +38,9 @@ Gordon registry, pushes it, and optionally deploys it.
3738 - The registry and repository are derived from the route image on the server.
3839 - The version tag defaults to the latest git tag. If no tag is found, `latest`
3940 is used.
40-- When `--build` is set, the command uses `docker buildx build --push` and
41- injects `VERSION=<tag>` plus any `--build-arg` values.
41+- When `--build` is set, the command builds with `docker buildx build --load`
42+ and injects `VERSION=<tag>` plus any `--build-arg` values.
43+- Use `-f/--file` to build from a Dockerfile outside the current directory root.
4244 - The version tag and `latest` are both pushed (unless the version is `latest`).
4345
4446 ### Examples
@@ -55,6 +57,9 @@ gordon push myapp.example.com --no-confirm
5557
5658 # Build for ARM and pass build args
5759 gordon push myapp.example.com --build --platform linux/arm64 --build-arg CGO_ENABLED=0
60+
61+# Build from a custom Dockerfile path
62+gordon push myapp.example.com --build -f docker/app/Dockerfile
5863 ```
5964
6065 ### Notes
@@ -21,6 +21,7 @@ gordon push <domain> [options]
21 | Option | Description |21 | Option | Description |
22 |--------|-------------|22 |--------|-------------|
23 | `--build` | Build the image first using `docker buildx` |23 | `--build` | Build the image first using `docker buildx` |
24+| `-f, --file` | Path to Dockerfile (default: `./Dockerfile`, used with `--build`) |
24 | `--platform` | Target platform for buildx (default: `linux/amd64`) |25 | `--platform` | Target platform for buildx (default: `linux/amd64`) |
25 | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) |26 | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) |
26 | `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) |27 | `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) |
@@ -37,8 +38,9 @@ Gordon registry, pushes it, and optionally deploys it.
37 - The registry and repository are derived from the route image on the server.38 - The registry and repository are derived from the route image on the server.
38 - The version tag defaults to the latest git tag. If no tag is found, `latest`39 - The version tag defaults to the latest git tag. If no tag is found, `latest`
39 is used.40 is used.
40-- When `--build` is set, the command uses `docker buildx build --push` and41+- When `--build` is set, the command builds with `docker buildx build --load`
41- injects `VERSION=<tag>` plus any `--build-arg` values.42+ and injects `VERSION=<tag>` plus any `--build-arg` values.
43+- Use `-f/--file` to build from a Dockerfile outside the current directory root.
42 - The version tag and `latest` are both pushed (unless the version is `latest`).44 - The version tag and `latest` are both pushed (unless the version is `latest`).
43 45
44 ### Examples46 ### Examples
@@ -55,6 +57,9 @@ gordon push myapp.example.com --no-confirm
55 57
56 # Build for ARM and pass build args58 # Build for ARM and pass build args
57 gordon push myapp.example.com --build --platform linux/arm64 --build-arg CGO_ENABLED=059 gordon push myapp.example.com --build --platform linux/arm64 --build-arg CGO_ENABLED=0
60+
61+# Build from a custom Dockerfile path
62+gordon push myapp.example.com --build -f docker/app/Dockerfile
58 ```63 ```
59 64
60 ### Notes65 ### Notes
M internal/adapters/in/cli/push.go +24 -14
@@ -18,12 +18,13 @@ import (
1818
1919 func newPushCmd() *cobra.Command {
2020 var (
21- noDeploy bool
22- noConfirm bool
23- build bool
24- platform string
25- tag string
26- buildArgs []string
21+ noDeploy bool
22+ noConfirm bool
23+ build bool
24+ platform string
25+ tag string
26+ dockerfile string
27+ buildArgs []string
2728 )
2829
2930 cmd := &cobra.Command{
@@ -41,7 +42,7 @@ Examples:
4142 gordon push myapp.example.com --build --build-arg CGO_ENABLED=0 --remote ...`,
4243 Args: cobra.ExactArgs(1),
4344 RunE: func(cmd *cobra.Command, args []string) error {
44- return runPush(cmd.Context(), args[0], tag, build, platform, buildArgs, noDeploy, noConfirm)
45+ return runPush(cmd.Context(), args[0], tag, build, platform, dockerfile, buildArgs, noDeploy, noConfirm)
4546 },
4647 }
4748
@@ -49,18 +50,26 @@ Examples:
4950 cmd.Flags().BoolVar(&noConfirm, "no-confirm", false, "Skip deploy confirmation prompt")
5051 cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")
5152 cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")
53+ cmd.Flags().StringVarP(&dockerfile, "file", "f", "", "Path to Dockerfile (default: ./Dockerfile, used with --build)")
5254 cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)")
5355 cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")
5456
5557 return cmd
5658 }
5759
58-func runPush(ctx context.Context, pushDomain, tag string, build bool, platform string, buildArgs []string, noDeploy bool, noConfirm bool) error {
60+func runPush(ctx context.Context, pushDomain, tag string, build bool, platform string, dockerfile string, buildArgs []string, noDeploy bool, noConfirm bool) error {
5961 client, isRemote := GetRemoteClient()
6062 if !isRemote {
6163 return fmt.Errorf("push requires remote mode")
6264 }
6365
66+ if dockerfile != "" && !build {
67+ return fmt.Errorf("--file can only be used with --build")
68+ }
69+ if dockerfile == "" {
70+ dockerfile = "Dockerfile"
71+ }
72+
6473 route, err := client.GetRoute(ctx, pushDomain)
6574 if err != nil {
6675 return fmt.Errorf("failed to get route: %w", err)
@@ -93,7 +102,7 @@ func runPush(ctx context.Context, pushDomain, tag string, build bool, platform s
93102 }
94103
95104 if build {
96- if err := buildAndPush(ctx, version, platform, buildArgs, versionRef, latestRef); err != nil {
105+ if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
97106 return err
98107 }
99108 } else {
@@ -122,9 +131,9 @@ func determineVersion(ctx context.Context, tag string) string {
122131 return version
123132 }
124133
125-func buildAndPush(ctx context.Context, version, platform string, buildArgs []string, versionRef, latestRef string) error {
126- if _, err := os.Stat("Dockerfile"); os.IsNotExist(err) {
127- return fmt.Errorf("no Dockerfile found in current directory")
134+func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
135+ if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
136+ return fmt.Errorf("Dockerfile not found: %s", dockerfile)
128137 }
129138
130139 // Build and load into local daemon (NOT --push).
@@ -132,7 +141,7 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
132141 // Cloudflare's 100MB per-request limit. Loading locally then
133142 // using docker push gives us chunked uploads (~5MB per request).
134143 fmt.Println("\nBuilding image...")
135- buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, buildArgs, versionRef, latestRef)...) // #nosec G204
144+ buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204
136145 buildCmd.Stdout = os.Stdout
137146 buildCmd.Stderr = os.Stderr
138147 if err := buildCmd.Run(); err != nil {
@@ -155,10 +164,11 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
155164 // buildImageArgs constructs the docker buildx build arguments.
156165 // Uses --load instead of --push so the image is loaded into the local
157166 // daemon, allowing docker push to handle the upload with chunked requests.
158-func buildImageArgs(version, platform string, buildArgs []string, versionRef, latestRef string) []string {
167+func buildImageArgs(version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) []string {
159168 args := []string{
160169 "buildx", "build",
161170 "--platform", platform,
171+ "-f", dockerfile,
162172 "-t", latestRef,
163173 "--build-arg", "VERSION=" + version,
164174 }
@@ -18,12 +18,13 @@ import (
18 18
19 func newPushCmd() *cobra.Command {19 func newPushCmd() *cobra.Command {
20 var (20 var (
21- noDeploy bool21+ noDeploy bool
22- noConfirm bool22+ noConfirm bool
23- build bool23+ build bool
24- platform string24+ platform string
25- tag string25+ tag string
26- buildArgs []string26+ dockerfile string
27+ buildArgs []string
27 )28 )
28 29
29 cmd := &cobra.Command{30 cmd := &cobra.Command{
@@ -41,7 +42,7 @@ Examples:
41 gordon push myapp.example.com --build --build-arg CGO_ENABLED=0 --remote ...`,42 gordon push myapp.example.com --build --build-arg CGO_ENABLED=0 --remote ...`,
42 Args: cobra.ExactArgs(1),43 Args: cobra.ExactArgs(1),
43 RunE: func(cmd *cobra.Command, args []string) error {44 RunE: func(cmd *cobra.Command, args []string) error {
44- return runPush(cmd.Context(), args[0], tag, build, platform, buildArgs, noDeploy, noConfirm)45+ return runPush(cmd.Context(), args[0], tag, build, platform, dockerfile, buildArgs, noDeploy, noConfirm)
45 },46 },
46 }47 }
47 48
@@ -49,18 +50,26 @@ Examples:
49 cmd.Flags().BoolVar(&noConfirm, "no-confirm", false, "Skip deploy confirmation prompt")50 cmd.Flags().BoolVar(&noConfirm, "no-confirm", false, "Skip deploy confirmation prompt")
50 cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")51 cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")
51 cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")52 cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")
53+ cmd.Flags().StringVarP(&dockerfile, "file", "f", "", "Path to Dockerfile (default: ./Dockerfile, used with --build)")
52 cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)")54 cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)")
53 cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")55 cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")
54 56
55 return cmd57 return cmd
56 }58 }
57 59
58-func runPush(ctx context.Context, pushDomain, tag string, build bool, platform string, buildArgs []string, noDeploy bool, noConfirm bool) error {60+func runPush(ctx context.Context, pushDomain, tag string, build bool, platform string, dockerfile string, buildArgs []string, noDeploy bool, noConfirm bool) error {
59 client, isRemote := GetRemoteClient()61 client, isRemote := GetRemoteClient()
60 if !isRemote {62 if !isRemote {
61 return fmt.Errorf("push requires remote mode")63 return fmt.Errorf("push requires remote mode")
62 }64 }
63 65
66+ if dockerfile != "" && !build {
67+ return fmt.Errorf("--file can only be used with --build")
68+ }
69+ if dockerfile == "" {
70+ dockerfile = "Dockerfile"
71+ }
72+
64 route, err := client.GetRoute(ctx, pushDomain)73 route, err := client.GetRoute(ctx, pushDomain)
65 if err != nil {74 if err != nil {
66 return fmt.Errorf("failed to get route: %w", err)75 return fmt.Errorf("failed to get route: %w", err)
@@ -93,7 +102,7 @@ func runPush(ctx context.Context, pushDomain, tag string, build bool, platform s
93 }102 }
94 103
95 if build {104 if build {
96- if err := buildAndPush(ctx, version, platform, buildArgs, versionRef, latestRef); err != nil {105+ if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
97 return err106 return err
98 }107 }
99 } else {108 } else {
@@ -122,9 +131,9 @@ func determineVersion(ctx context.Context, tag string) string {
122 return version131 return version
123 }132 }
124 133
125-func buildAndPush(ctx context.Context, version, platform string, buildArgs []string, versionRef, latestRef string) error {134+func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
126- if _, err := os.Stat("Dockerfile"); os.IsNotExist(err) {135+ if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
127- return fmt.Errorf("no Dockerfile found in current directory")136+ return fmt.Errorf("Dockerfile not found: %s", dockerfile)
128 }137 }
129 138
130 // Build and load into local daemon (NOT --push).139 // Build and load into local daemon (NOT --push).
@@ -132,7 +141,7 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
132 // Cloudflare's 100MB per-request limit. Loading locally then141 // Cloudflare's 100MB per-request limit. Loading locally then
133 // using docker push gives us chunked uploads (~5MB per request).142 // using docker push gives us chunked uploads (~5MB per request).
134 fmt.Println("\nBuilding image...")143 fmt.Println("\nBuilding image...")
135- buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, buildArgs, versionRef, latestRef)...) // #nosec G204144+ buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204
136 buildCmd.Stdout = os.Stdout145 buildCmd.Stdout = os.Stdout
137 buildCmd.Stderr = os.Stderr146 buildCmd.Stderr = os.Stderr
138 if err := buildCmd.Run(); err != nil {147 if err := buildCmd.Run(); err != nil {
@@ -155,10 +164,11 @@ func buildAndPush(ctx context.Context, version, platform string, buildArgs []str
155 // buildImageArgs constructs the docker buildx build arguments.164 // buildImageArgs constructs the docker buildx build arguments.
156 // Uses --load instead of --push so the image is loaded into the local165 // 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.166 // daemon, allowing docker push to handle the upload with chunked requests.
158-func buildImageArgs(version, platform string, buildArgs []string, versionRef, latestRef string) []string {167+func buildImageArgs(version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) []string {
159 args := []string{168 args := []string{
160 "buildx", "build",169 "buildx", "build",
161 "--platform", platform,170 "--platform", platform,
171+ "-f", dockerfile,
162 "-t", latestRef,172 "-t", latestRef,
163 "--build-arg", "VERSION=" + version,173 "--build-arg", "VERSION=" + version,
164 }174 }
M internal/adapters/in/cli/push_test.go +10 -1
@@ -63,9 +63,18 @@ func TestParseImageRef(t *testing.T) {
6363
6464 func TestBuildAndPush_BuildArgs(t *testing.T) {
6565 // 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")
66+ args := buildImageArgs("v1.0.0", "linux/amd64", "Dockerfile", []string{"CGO_ENABLED=0"}, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
6767
6868 assert.Contains(t, args, "--load")
6969 assert.NotContains(t, args, "--push")
7070 assert.Contains(t, args, "--platform")
71+ assert.Contains(t, args, "-f")
72+ assert.Contains(t, args, "Dockerfile")
73+}
74+
75+func TestBuildImageArgs_CustomDockerfile(t *testing.T) {
76+ args := buildImageArgs("v1.0.0", "linux/amd64", "docker/app/Dockerfile", nil, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
77+
78+ assert.Contains(t, args, "-f")
79+ assert.Contains(t, args, "docker/app/Dockerfile")
7180 }
@@ -63,9 +63,18 @@ func TestParseImageRef(t *testing.T) {
63 63
64 func TestBuildAndPush_BuildArgs(t *testing.T) {64 func TestBuildAndPush_BuildArgs(t *testing.T) {
65 // Verify buildImageArgs produces --load instead of --push65 // 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")66+ args := buildImageArgs("v1.0.0", "linux/amd64", "Dockerfile", []string{"CGO_ENABLED=0"}, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
67 67
68 assert.Contains(t, args, "--load")68 assert.Contains(t, args, "--load")
69 assert.NotContains(t, args, "--push")69 assert.NotContains(t, args, "--push")
70 assert.Contains(t, args, "--platform")70 assert.Contains(t, args, "--platform")
71+ assert.Contains(t, args, "-f")
72+ assert.Contains(t, args, "Dockerfile")
73+}
74+
75+func TestBuildImageArgs_CustomDockerfile(t *testing.T) {
76+ args := buildImageArgs("v1.0.0", "linux/amd64", "docker/app/Dockerfile", nil, "reg.example.com/app:v1.0.0", "reg.example.com/app:latest")
77+
78+ assert.Contains(t, args, "-f")
79+ assert.Contains(t, args, "docker/app/Dockerfile")
71 }80 }

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