diff --git a/.tool-versions b/.tool-versions index 463dd77..51d646e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -golang 1.21 +golang 1.21.5 \ No newline at end of file diff --git a/README.md b/README.md index ddc92de..c546ac7 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,39 @@ Node.js, Python, Ruby, Java/Spring Boot, Go, Elixir/Phoenix, and more. For detailed documentation, visit the [FlexStack Documentation](https://flexstack.com/docs/languages-and-frameworks/autogenerate-dockerfile) page. +## Features + +- [x] Automatically detect the runtime and framework used by your project +- [x] Use version managers like [asdf](https://github.com/asdf-vm), nvm, rbenv, and pyenv to install the correct version of the runtime +- [x] Make a best effort to detect any install, build, and run commands +- [x] Generate a Dockerfile with sensible defaults that are configurable via [Docker Build Args](https://docs.docker.com/build/guide/build-args/) +- [x] Support for a wide range of the most popular languages and frameworks including Next.js, Phoenix, Spring Boot, Django, and more +- [x] Use Debian Slim as the runtime image for a smaller image size and better security, while still supporting the most common dependencies and avoiding deployment headaches caused by Alpine Linux gotchas +- [x] Use multi-stage builds to reduce the size of the final image +- [x] Supports multi-platform images that run on both x86 and ARM CPU architectures + +## Supported Runtimes + +- Bun +- Deno +- Docker +- Elixir +- Go +- Java +- Next.js +- Node.js +- PHP +- Python +- Ruby +- Rust +- Static (HTML, CSS, JS) + ## Installation ### cURL ```sh -curl -sSL https://flexstack.com/install/new-dockerfile | sh +curl -fsSL https://flexstack.com/install/new-dockerfile | bash ``` ### Go Package @@ -20,12 +47,6 @@ curl -sSL https://flexstack.com/install/new-dockerfile | sh go get github.com/flexstack/new-dockerfile ``` -## Supported Platforms for CLI - -- **macOS** (arm64, x86_64) -- **Linux** (arm64, x86_64) -- **Windows** (x86_64, i386) - ## CLI Usage ```sh @@ -38,4 +59,38 @@ new-dockerfile [options] - `--write` - Write the generated Dockerfile to the project at the specified path (default: `false`) - `--runtime` - Force a specific runtime, e.g. `node` (default: `auto`) - `--quiet` - Disable all logging except for errors (default: `false`) -- `--help` - Show help \ No newline at end of file +- `--help` - Show help + +## CLI Examples + +Write a Dockerfile to the current directory: +```sh +new-dockerfile --write +``` + +Write a Dockerfile to a specific directory: +```sh +new-dockerfile > path/to/Dockerfile +``` + +Force a specific runtime: +```sh +new-dockerfile --runtime next.js +``` + +List the supported runtimes: +```sh +new-dockerfile --runtime list +``` + +## How it Works + +The tool searches for common files and directories in your project to determine the runtime and framework. +For example, if it finds a `package.json` file, it will assume the project is a Node.js project unless +a `next.config.js` file is present, in which case it will assume the project is a Next.js project. + +From there, it will read any `.tool-versions` or other version manager files to determine the correct version +of the runtime to install. It will then make a best effort to detect any install, build, and run commands. +For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the run command. + +Read on to see runtime-specific examples and how to configure the generated Dockerfile. \ No newline at end of file diff --git a/cmd/new-dockerfile/main.go b/cmd/new-dockerfile/main.go index 504d52a..75bd2a1 100644 --- a/cmd/new-dockerfile/main.go +++ b/cmd/new-dockerfile/main.go @@ -52,7 +52,7 @@ func main() { runtimes := df.ListRuntimes() for _, rt := range runtimes { - if strings.ToLower(string(rt.Name())) == strings.ToLower(runtimeArg) { + if strings.EqualFold(string(rt.Name()), runtimeArg) { r = rt break } @@ -60,8 +60,15 @@ func main() { if r == nil { runtimeNames := make([]string, len(runtimes)) for i, rt := range runtimes { - runtimeNames[i] = string(rt.Name()) + runtimeNames[i] = strings.ToLower(string(rt.Name())) } + + if runtimeArg == "list" { + fmt.Println("Available runtimes:") + fmt.Println(" - " + strings.Join(runtimeNames, "\n - ")) + os.Exit(0) + } + log.Error(fmt.Sprintf(`Runtime "%s" not found. Expected one of: %s`, runtimeArg, "\n - "+strings.Join(runtimeNames, "\n - "))) os.Exit(1) } diff --git a/node/README.md b/node/README.md index 9975398..4b48dc2 100644 --- a/node/README.md +++ b/node/README.md @@ -1,27 +1,90 @@ # Autogenerate a Dockerfile -FlexStack's `new-dockerfile` CLI tool automatically generates a configurable Dockerfile based on your project source code. -It supports a wide range of languages and frameworks, including Next.js, Node.js, Python, Ruby, Java/Spring Boot, Go, -Elixir/Phoenix, and more. +FlexStack's `new-dockerfile` CLI tool and Go package automatically generates a configurable Dockerfile +based on your project source code. It supports a wide range of languages and frameworks, including Next.js, +Node.js, Python, Ruby, Java/Spring Boot, Go, Elixir/Phoenix, and more. For detailed documentation, visit the [FlexStack Documentation](https://flexstack.com/docs/languages-and-frameworks/autogenerate-dockerfile) page. -## Supported Platforms +## Features -- **macOS** (arm64, x86_64) -- **Linux** (arm64, x86_64) -- **Windows** (x86_64, i386) +- [x] Automatically detect the runtime and framework used by your project +- [x] Use version managers like [asdf](https://github.com/asdf-vm), nvm, rbenv, and pyenv to install the correct version of the runtime +- [x] Make a best effort to detect any install, build, and run commands +- [x] Generate a Dockerfile with sensible defaults that are configurable via [Docker Build Args](https://docs.docker.com/build/guide/build-args/) +- [x] Support for a wide range of the most popular languages and frameworks including Next.js, Phoenix, Spring Boot, Django, and more +- [x] Use Debian Slim as the runtime image for a smaller image size and better security, while still supporting the most common dependencies and avoiding deployment headaches caused by Alpine Linux gotchas +- [x] Use multi-stage builds to reduce the size of the final image +- [x] Supports multi-platform images that run on both x86 and ARM CPU architectures + +## Supported Runtimes + +- Bun +- Deno +- Docker +- Elixir +- Go +- Java +- Next.js +- Node.js +- PHP +- Python +- Ruby +- Rust +- Static (HTML, CSS, JS) ## Usage +Using `npx`: + ```sh npx new-dockerfile [options] ``` +Install the CLI globally: + +```sh +npm install -g new-dockerfile +``` + ## Options - `--path` - Path to the project source code (default: `.`) - `--write` - Write the generated Dockerfile to the project at the specified path (default: `false`) - `--runtime` - Force a specific runtime, e.g. `node` (default: `auto`) - `--quiet` - Disable all logging except for errors (default: `false`) -- `--help` - Show help \ No newline at end of file +- `--help` - Show help + +## Examples + +Write a Dockerfile to the current directory: +```sh +new-dockerfile --write +``` + +Write a Dockerfile to a specific directory: +```sh +new-dockerfile > path/to/Dockerfile +``` + +Force a specific runtime: +```sh +new-dockerfile --runtime next.js +``` + +List the supported runtimes: +```sh +new-dockerfile --runtime list +``` + +## How it Works + +The tool searches for common files and directories in your project to determine the runtime and framework. +For example, if it finds a `package.json` file, it will assume the project is a Node.js project unless +a `next.config.js` file is present, in which case it will assume the project is a Next.js project. + +From there, it will read any `.tool-versions` or other version manager files to determine the correct version +of the runtime to install. It will then make a best effort to detect any install, build, and run commands. +For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the run command. + +Read on to see runtime-specific examples and how to configure the generated Dockerfile. \ No newline at end of file diff --git a/runtime/bun.go b/runtime/bun.go index 267d1ec..d91f90d 100644 --- a/runtime/bun.go +++ b/runtime/bun.go @@ -67,28 +67,22 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) { if ok { d.Log.Info("Detected scripts in package.json") - if _, ok := scripts["start:prod"].(string); ok { - startCMD = "bun run start:prod" - } else if _, ok := scripts["start:production"].(string); ok { - startCMD = "bun run start:production" - } else if _, ok := scripts["start-prod"].(string); ok { - startCMD = "bun run start-prod" - } else if _, ok := scripts["start-production"].(string); ok { - startCMD = "bun run start-production" - } else if _, ok := scripts["start"].(string); ok { - startCMD = "bun run start" + startCommands := []string{"serve", "start:prod", "start:production", "start-prod", "start-production", "start"} + for _, cmd := range startCommands { + if _, ok := scripts[cmd].(string); ok { + d.Log.Info("Detected start command in package.json: " + cmd) + startCMD = fmt.Sprintf("bun run %s", cmd) + break + } } - if _, ok := scripts["build:prod"].(string); ok { - buildCMD = "bun run build:prod" - } else if _, ok := scripts["build:production"].(string); ok { - buildCMD = "bun run build:production" - } else if _, ok := scripts["build-prod"].(string); ok { - buildCMD = "bun run build-prod" - } else if _, ok := scripts["build-production"].(string); ok { - buildCMD = "bun run build-production" - } else if _, ok := scripts["build"].(string); ok { - buildCMD = "bun run build" + buildCommands := []string{"build:prod", "build:production", "build-prod", "build-production", "build"} + for _, cmd := range buildCommands { + if _, ok := scripts[cmd].(string); ok { + d.Log.Info("Detected build command in package.json: " + cmd) + buildCMD = fmt.Sprintf("bun run %s", cmd) + break + } } } @@ -100,6 +94,7 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) { } if startCMD == "" && mainFile != "" { + d.Log.Info("Detected start command via main file: " + mainFile) startCMD = fmt.Sprintf("bun %s", mainFile) } @@ -124,17 +119,11 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) { startCMD = string(startCMDJSON) } - finalVersion := "slim" - if *version != "latest" { - finalVersion = *version + "-slim" - } - var buf bytes.Buffer if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ - "Version": *version, - "FinalVersion": finalVersion, - "BuildCMD": buildCMD, - "StartCMD": startCMD, + "Version": *version, + "BuildCMD": buildCMD, + "StartCMD": startCMD, }); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -162,7 +151,7 @@ ARG BUILD_CMD={{.BuildCMD}} ENV BUILD_CMD=${BUILD_CMD} RUN if [ ! -z "${BUILD_CMD}" ]; then $BUILD_CMD; fi -FROM oven/bun:{{.FinalVersion}} AS final +FROM oven/bun:${VERSION}-slim AS runtime WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends wget && apt-get clean && rm -f /var/lib/apt/lists/*_* @@ -224,7 +213,7 @@ func findBunVersion(path string, log *slog.Logger) (*string, error) { } if version == "" { - version = "latest" + version = "1" log.Info(fmt.Sprintf("No Bun version detected. Using: %s", version)) } diff --git a/runtime/deno.go b/runtime/deno.go index dc9dd44..d37385b 100644 --- a/runtime/deno.go +++ b/runtime/deno.go @@ -97,16 +97,17 @@ func (d *Deno) GenerateDockerfile(path string) ([]byte, error) { scripts, ok := denoJSON["tasks"].(map[string]interface{}) if ok { - d.Log.Info("Detected tasks in deno.json") - startCommands := []string{"start:prod", "start:production", "start-prod", "start-production", "start"} + startCommands := []string{"serve", "start:prod", "start:production", "start-prod", "start-production", "start"} for _, cmd := range startCommands { if _, ok := scripts[cmd].(string); ok { + d.Log.Info("Detected start command in deno.json: " + cmd) startCMD = fmt.Sprintf("deno task %s", cmd) break } } if _, ok := scripts["cache"].(string); ok { + d.Log.Info("Detected install command in deno.json: cache") installCMD = "deno task cache" } } @@ -115,8 +116,11 @@ func (d *Deno) GenerateDockerfile(path string) ([]byte, error) { mainFiles := []string{"mod.ts", "src/mod.ts", "main.ts", "src/main.ts", "index.ts", "src/index.ts"} for _, mainFile := range mainFiles { if _, err := os.Stat(filepath.Join(path, mainFile)); err == nil { + d.Log.Info("Detected start command via main/mod file: " + mainFile) + startCMD = fmt.Sprintf("deno run --allow-all %s", mainFile) if installCMD == "" { + d.Log.Info("Detected install command via main/mod file: " + mainFile) installCMD = "deno cache " + mainFile } break diff --git a/runtime/node.go b/runtime/node.go index bf83159..2c362f3 100644 --- a/runtime/node.go +++ b/runtime/node.go @@ -78,7 +78,7 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { scripts, ok := packageJSON["scripts"].(map[string]interface{}) if ok { d.Log.Info("Detected scripts in package.json") - startCommands := []string{"start:prod", "start:production", "start-prod", "start-production", "start"} + startCommands := []string{"serve", "start:prod", "start:production", "start-prod", "start-production", "start"} for _, cmd := range startCommands { if _, ok := scripts[cmd].(string); ok { startCMD = fmt.Sprintf("%s run %s", packageManager, cmd) @@ -176,7 +176,7 @@ ENV NODE_ENV=production ARG BUILD_CMD={{.BuildCMD}} RUN if [ ! -z "${BUILD_CMD}" ]; then $BUILD_CMD; fi -FROM base AS final +FROM base AS runtime WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends wget && apt-get clean && rm -f /var/lib/apt/lists/*_*