Skip to content

Commit a55a74b

Browse files
authored
[gen] Implement gen prod dockerfile (#2017)
## Summary Simple implementation for generating a prod dockerfile from a devbox project. Notes: * Flag to create this is hidden. * Currently this requires a `start` script. There are also optional `install` and `build` scripts. Follow ups: * In addition to `start` script I want to support services but I was having trouble getting process compose to start as Dockerfile CMD. Will debug further and add in follow up. * We can greatly optimize the Dockerfile by downloading devbox dependencies first before coping source code. This will likely require a new devbox command or flag. ## How was it tested? Tested using this project https://github.com/mikeland73/hello-world-server ```bash devbox generate dockerfile --for prod docker build . -t hello-world docker run -p 8080:8080 hello-world ```
1 parent e46ff12 commit a55a74b

File tree

6 files changed

+113
-20
lines changed

6 files changed

+113
-20
lines changed

internal/boxcli/generate.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type generateCmdFlags struct {
2828
rootUser bool
2929
}
3030

31+
type generateDockerfileCmdFlags struct {
32+
generateCmdFlags
33+
forType string
34+
}
35+
3136
type GenerateReadmeCmdFlags struct {
3237
generateCmdFlags
3338
saveTemplate bool
@@ -94,17 +99,33 @@ func devcontainerCmd() *cobra.Command {
9499
}
95100

96101
func dockerfileCmd() *cobra.Command {
97-
flags := &generateCmdFlags{}
102+
flags := &generateDockerfileCmdFlags{}
98103
command := &cobra.Command{
99104
Use: "dockerfile",
100105
Short: "Generate a Dockerfile that replicates devbox shell",
101106
Long: "Generate a Dockerfile that replicates devbox shell. " +
102107
"Can be used to run devbox shell environment in an OCI container.",
103108
Args: cobra.MaximumNArgs(0),
104109
RunE: func(cmd *cobra.Command, args []string) error {
105-
return runGenerateCmd(cmd, flags)
110+
box, err := devbox.Open(&devopt.Opts{
111+
Dir: flags.config.path,
112+
Environment: flags.config.environment,
113+
Stderr: cmd.ErrOrStderr(),
114+
})
115+
if err != nil {
116+
return errors.WithStack(err)
117+
}
118+
return box.GenerateDockerfile(cmd.Context(), devopt.GenerateOpts{
119+
ForType: flags.forType,
120+
Force: flags.force,
121+
RootUser: flags.rootUser,
122+
})
106123
},
107124
}
125+
command.Flags().StringVar(
126+
&flags.forType, "for", "dev",
127+
"Generate Dockerfile for a specific type of container (dev, prod)")
128+
command.Flag("for").Hidden = true
108129
command.Flags().BoolVarP(
109130
&flags.force, "force", "f", false, "force overwrite existing files")
110131
command.Flags().BoolVar(
@@ -264,8 +285,6 @@ func runGenerateCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
264285
return box.Generate(cmd.Context())
265286
case "devcontainer":
266287
return box.GenerateDevcontainer(cmd.Context(), generateOpts)
267-
case "dockerfile":
268-
return box.GenerateDockerfile(cmd.Context(), generateOpts)
269288
}
270289
return nil
271290
}

internal/devbox/devbox.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ func (d *Devbox) GenerateDevcontainer(ctx context.Context, generateOpts devopt.G
451451
}
452452

453453
// generate dockerfile
454-
err = gen.CreateDockerfile(ctx)
454+
err = gen.CreateDockerfile(ctx, generate.CreateDockerfileOptions{})
455455
if err != nil {
456456
return redact.Errorf("error generating dev container Dockerfile in <project>/%s: %w",
457457
redact.Safe(filepath.Base(devContainerPath)), err)
@@ -489,8 +489,15 @@ func (d *Devbox) GenerateDockerfile(ctx context.Context, generateOpts devopt.Gen
489489
LocalFlakeDirs: d.getLocalFlakesDirs(),
490490
}
491491

492+
scripts := d.cfg.Scripts()
493+
492494
// generate dockerfile
493-
return errors.WithStack(gen.CreateDockerfile(ctx))
495+
return errors.WithStack(gen.CreateDockerfile(ctx, generate.CreateDockerfileOptions{
496+
ForType: generateOpts.ForType,
497+
HasBuild: scripts["build"] != nil,
498+
HasInstall: scripts["install"] != nil,
499+
HasStart: scripts["start"] != nil,
500+
}))
494501
}
495502

496503
func PrintEnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {

internal/devbox/devopt/devboxopts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Opts struct {
2020
}
2121

2222
type GenerateOpts struct {
23+
ForType string
2324
Force bool
2425
RootUser bool
2526
}

internal/devbox/generate/devcontainer_util.go

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ package generate
66
// package generate has functionality to implement the `devbox generate` command
77

88
import (
9+
"cmp"
910
"context"
1011
"embed"
1112
"encoding/json"
1213
"fmt"
13-
"html/template"
1414
"io"
1515
"os"
1616
"path/filepath"
1717
"regexp"
1818
"runtime/trace"
1919
"strings"
20+
"text/template"
2021

22+
"github.com/samber/lo"
23+
"go.jetpack.io/devbox/internal/boxcli/usererr"
2124
"go.jetpack.io/devbox/internal/debug"
2225
"go.jetpack.io/devbox/internal/devbox/devopt"
2326
)
@@ -54,30 +57,65 @@ type vscode struct {
5457
Extensions []string `json:"extensions"`
5558
}
5659

57-
type dockerfileData struct {
58-
IsDevcontainer bool
59-
RootUser bool
60-
LocalFlakeDirs []string
60+
type CreateDockerfileOptions struct {
61+
ForType string
62+
HasInstall bool
63+
HasBuild bool
64+
HasStart bool
65+
// Ideally we also support process-compose services as the dockerfile
66+
// CMD, but I'm currently having trouble getting that to work. Will revisit.
67+
// HasServices bool
68+
}
69+
70+
func (opts CreateDockerfileOptions) Type() string {
71+
return cmp.Or(opts.ForType, "dev")
72+
}
73+
74+
func (opts CreateDockerfileOptions) validate() error {
75+
if opts.Type() == "dev" {
76+
return nil
77+
} else if opts.Type() == "prod" {
78+
if opts.HasStart {
79+
return nil
80+
}
81+
return usererr.New(
82+
"To generate a prod Dockerfile you must have either 'start' script in " +
83+
"devbox.json",
84+
)
85+
}
86+
return usererr.New(
87+
"invalid Dockerfile type. Only 'dev' and 'prod' are supported")
6188
}
6289

63-
// CreateDockerfile creates a Dockerfile in path and writes devcontainerDockerfile.tmpl's content into it
64-
func (g *Options) CreateDockerfile(ctx context.Context) error {
90+
// CreateDockerfile creates a Dockerfile in path.
91+
func (g *Options) CreateDockerfile(
92+
ctx context.Context,
93+
opts CreateDockerfileOptions,
94+
) error {
6595
defer trace.StartRegion(ctx, "createDockerfile").End()
6696

97+
if err := opts.validate(); err != nil {
98+
return err
99+
}
100+
67101
// create dockerfile
68102
file, err := os.Create(filepath.Join(g.Path, "Dockerfile"))
69103
if err != nil {
70104
return err
71105
}
72106
defer file.Close()
73-
// get dockerfile content
74-
tmplName := "devcontainerDockerfile.tmpl"
75-
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
107+
path := fmt.Sprintf("tmpl/%s.Dockerfile.tmpl", opts.Type())
108+
t := template.Must(template.ParseFS(tmplFS, path))
76109
// write content into file
77-
return t.Execute(file, &dockerfileData{
78-
IsDevcontainer: g.IsDevcontainer,
79-
RootUser: g.RootUser,
80-
LocalFlakeDirs: g.LocalFlakeDirs,
110+
return t.Execute(file, map[string]any{
111+
"IsDevcontainer": g.IsDevcontainer,
112+
"RootUser": g.RootUser,
113+
"LocalFlakeDirs": g.LocalFlakeDirs,
114+
115+
// The following are only used for prod Dockerfile
116+
"DevboxRunInstall": lo.Ternary(opts.HasInstall, "devbox run install", "echo 'No install script found, skipping'"),
117+
"DevboxRunBuild": lo.Ternary(opts.HasBuild, "devbox run build", "echo 'No build script found, skipping'"),
118+
"Cmd": fmt.Sprintf("%q, %q, %q", "devbox", "run", "start"),
81119
})
82120
}
83121

File renamed without changes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM jetpackio/devbox:latest
2+
3+
WORKDIR /code
4+
USER root:root
5+
RUN mkdir -p /code && chown ${DEVBOX_USER}:${DEVBOX_USER} /code
6+
USER ${DEVBOX_USER}:${DEVBOX_USER}
7+
8+
{{- /*
9+
Ideally, we first copy over devbox.json and devbox.lock and run `devbox install`
10+
to create a cache layer for the dependencies. This is complicated because
11+
devbox.json may include local dependencies (flakes and plugins). We could try
12+
to copy those in (the way the dev Dockerfile does) but that's brittle because
13+
those dependencies may also pull in other local dependencies and so on. Another
14+
sulution would be to add a new flag `devbox install --skip-errors` that would
15+
just try to install what it can, and ignore the rest.
16+
17+
A hack to make this simpler is to install from the lockfile instead of the json.
18+
*/}}
19+
20+
COPY --chown=${DEVBOX_USER}:${DEVBOX_USER} . .
21+
22+
RUN devbox install
23+
24+
RUN {{ .DevboxRunInstall }}
25+
26+
RUN {{ .DevboxRunBuild }}
27+
28+
CMD [{{ .Cmd }}]

0 commit comments

Comments
 (0)