docksmith

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

Docksmith

Go library and CLI that detects your framework and generates a hardened, multi-stage Dockerfile. Two dependencies. No lock-in.

At a glance

Type Go library + CLI
Module github.com/permanu/docksmith
Go version 1.26+
Dependencies 2 (BurntSushi/toml, yaml.v3)
Runtimes 12 (Node, Python, Go, Ruby, Rust, Java, PHP, .NET, Elixir, Deno, Bun, Static)
Detectors 45 built-in + static fallback
Architecture Detect → Plan → Emit (each independently usable)
Output Plain Dockerfile (committable, no lock-in)
API docs pkg.go.dev/github.com/permanu/docksmith
License Apache 2.0
Status Pre-release; internal use at Permanu
$ docksmith detect .
framework:  express
port:       3000
node:       22
pm:         npm

$ docksmith dockerfile . > Dockerfile

What comes out

Point docksmith at an Express app with a package.json and it produces this:

# syntax=docker/dockerfile:1

FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm \
    if [ -f package-lock.json ]; then npm ci || npm install; else npm install; fi
RUN apk add --no-cache tini

FROM deps AS build
COPY . .
RUN npm install

FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=build --link /app /app
CMD ["npm", "start"]
COPY --from=deps /sbin/tini /sbin/tini
ENTRYPOINT ["/sbin/tini", "--"]
USER node
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
    CMD node -e "const http=require('http');http.get('http://localhost:3000/', \
    r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"

Things to notice: multi-stage build, BuildKit cache mounts, tini as PID 1, non-root user, health check using only Node stdlib (no curl needed). No manual work.

For Go, it uses distroless -- zero shell attack surface:

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum* ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /app/app .

FROM gcr.io/distroless/static-debian12:nonroot AS runtime
WORKDIR /app
COPY --from=builder /app/app ./app
USER nonroot
EXPOSE 8080
CMD ["./app"]

Install

Requirements: Go 1.26+ (for library/CLI install). Docker only needed for docksmith build.

go install github.com/permanu/docksmith/cmd/docksmith@latest

Or grab a binary from the releases page.

Quick start

docksmith detect .                   # what framework is this?
docksmith dockerfile . > Dockerfile  # generate it
docker build -t myapp .             # build normally

Other commands: plan (inspect the build plan), eject (write Dockerfile + .dockerignore to disk), init (generate a docksmith.toml pre-filled from detection), build (detect + generate + docker build in one shot), registry search (find community framework definitions), registry install (install a YAML definition from the registry).

How it works

Three stages, each independently usable:

  1. Detect -- scans your project for package.json, go.mod, requirements.txt, Cargo.toml, etc. Identifies the framework, runtime version, and package manager. 45 detectors, each a small function.
  2. Plan -- takes the detection result and builds a BuildPlan: which base images, which stages, what commands, plus hardening (non-root user, tini, health checks, distroless where possible, BuildKit cache mounts).
  3. Emit -- serializes the plan into a Dockerfile string. Standard Dockerfile syntax. No proprietary format, no runtime dependency.

The entire library has two external dependencies (a TOML parser and a YAML parser). No Docker client, no container runtime, no network calls during detection or generation.

The output is a plain Dockerfile. You can commit it, modify it, or throw it away. No lock-in.

What the generated Dockerfile includes

Every generated Dockerfile gets these by default:

  • Multi-stage builds -- separate dependency, build, and runtime stages. Only runtime artifacts end up in the final image.
  • BuildKit cache mounts -- --mount=type=cache for package manager caches (npm, pip, Go modules, etc.), so rebuilds reuse downloaded dependencies.
  • Non-root user -- the final stage always runs as a non-root user (node, nonroot, etc. depending on the runtime).
  • Tini as PID 1 -- Node.js and Python containers use tini to handle signals and zombie processes correctly.
  • Distroless runtime -- Go and Rust containers use gcr.io/distroless/static for the runtime stage. No shell, no package manager, minimal attack surface.
  • Health checks -- auto-injected per runtime. Node uses stdlib http.get, Python uses urllib, Java uses wget. Go distroless containers do not get a health check (no shell to run it in).

Comparison

Docksmith Railpack Nixpacks Cloud Native Buildpacks
Approach Generates Dockerfiles Builds OCI images via BuildKit LLB Generates Dockerfiles Builds OCI images
Written in Go Go Rust Go/Java
Output Readable, committable Dockerfile OCI image (no intermediate Dockerfile) Dockerfile Opaque OCI layers
Use as library Yes (Go API) Go API + CLI CLI only CLI + pack API
Languages 12 runtimes, 45 framework detectors 11 language providers 23 language providers 9 language families (Paketo)
Multi-stage builds Always BuildKit layers (implicit) Partial No
Non-root user Always Depends on provider Sometimes Sometimes
Health checks Auto-injected per runtime No No No
Tini init Node, Python No No No
Distroless Go, Rust No No No
Monorepo support Yes (--root flag separates context from app dir) Yes (workspace detection) Limited Varies
Buildtime secrets Yes (BuildKit --mount=type=secret) Yes (BuildKit secrets) No Varies
Runtime mgmt Alpine apk Mise Nix Buildpack-provided
Custom frameworks YAML definitions + Go API Provider plugins (Go) Nix expressions Buildpacks (complex)
Status Internal testing at Permanu Powers all Railway deployments Maintenance mode Mature, wide adoption
License Apache 2.0 MIT MIT Apache 2.0
Comparison notes

Railpack — Railway's successor to Nixpacks (January 2025). Builds OCI images directly via BuildKit LLB using Mise for runtime management. Supports monorepos, BuildKit secrets, and SPA frameworks. Railway reports 38% smaller Node and 77% smaller Python images vs Nixpacks.

  • Stronger than Docksmith: proven at Railway's scale, smaller images (graph-based parallel BuildKit execution), SPA-specific optimizations (asset hashing, CDN headers, fallback routing)
  • Docksmith differs: readable/committable Dockerfile output, embeddable Go library, hardening defaults (tini, distroless, health checks), community YAML framework registry

Nixpacks — Maintenance mode. Railway recommends Railpack. Widest language coverage (23 providers including Crystal, Haskell, Dart, Zig, and others docksmith does not support).

Cloud Native Buildpacks / Paketo — Most mature option. CNCF project. Used by Heroku, Google Cloud, and Spring Boot. Tradeoff: opaque OCI layers — you cannot inspect or modify the generated layers the way you can with a Dockerfile.

Docksmith — Choose when you want readable Dockerfiles, a Go library to embed in your own platform, or hardening defaults without manual work. Supports monorepos (--root), BuildKit secret mounts for private registries, and a community YAML framework registry. Does not build images itself — generates Dockerfiles and hands off to Docker/BuildKit.

Supported frameworks

Runtime Frameworks
Node.js Next.js, Nuxt, SvelteKit, Astro, Remix, Gatsby, Vite, Create React App, Angular, Vue CLI, SolidStart, NestJS, Express, Fastify
Python Django, FastAPI, Flask
Go Gin, Echo, Fiber, net/http
Ruby Rails, Sinatra
Rust Actix, Axum
Java Spring Boot, Quarkus, Micronaut, Maven (generic), Gradle (generic)
PHP Laravel, Symfony, Slim, WordPress, plain PHP
.NET ASP.NET Core, Blazor, Worker
Elixir Phoenix
Deno Fresh, Oak, plain Deno
Bun Elysia, Hono, plain Bun
Static HTML/CSS/JS (served via nginx, detected as fallback)

Detection is priority-ordered. More specific frameworks (e.g., Next.js) are checked before generic ones (e.g., plain Node). If nothing matches and the directory has no web-servable content, docksmith returns an error -- it does not silently generate a broken Dockerfile.

Library usage

Docksmith is a Go library first. The CLI is a thin wrapper. Full API documentation is on pkg.go.dev.

import "github.com/permanu/docksmith"

// Detect framework
fw, err := docksmith.Detect("./my-project")

// Generate build plan
plan, err := docksmith.Plan(fw)

// Emit Dockerfile
dockerfile := docksmith.EmitDockerfile(plan)

One-shot, with overrides:

dockerfile, fw, err := docksmith.Build("./my-project",
    docksmith.WithExpose(8080),
    docksmith.WithStartCommand("./server"),
    docksmith.WithExtraEnv(map[string]string{"GIN_MODE": "release"}),
)

Available options: WithUser, WithHealthcheck, WithHealthcheckDisabled, WithRuntimeImage, WithBaseImage, WithEntrypoint, WithExtraEnv, WithExpose, WithInstallCommand, WithBuildCommand, WithStartCommand, WithSystemDeps, WithBuildCacheDisabled, WithSecrets, WithContextRoot.

Each subpackage (detect, plan, emit, config, yamldef) is independently importable:

import (
    "github.com/permanu/docksmith/detect"
    "github.com/permanu/docksmith/plan"
    "github.com/permanu/docksmith/emit"
)

// Use detect alone — CI tools that need framework metadata
fw, _ := detect.Detect("./my-project")
fmt.Println(fw.Name, fw.Port) // "nextjs", 3000

// Use plan alone — tools that need build step info
bp, _ := plan.Plan(fw)
fmt.Println(len(bp.Stages)) // 3 (deps, build, runtime)

// Use emit alone — construct plans programmatically
dockerfile := emit.EmitDockerfile(customPlan)

Configuration

Create docksmith.toml in your project root to override detected defaults:

runtime = "node"
version = "22"

[build]
command = "bun run build"

[start]
command = "bun run start"

[install]
command = "bun install --frozen-lockfile"
system_deps = ["libpq-dev"]

[runtime_config]
image = "node:22-alpine"
expose = 3000

[env]
NODE_ENV = "production"

Monorepo support -- separate build context from app directory:

context_root = "."  # repo root as Docker build context
# run: docksmith dockerfile --root . ./apps/frontend

Buildtime secrets for private registries and API keys:

[secrets]
npm = { target = "/root/.npmrc" }
license_key = { env = "LICENSE_KEY" }

When private registry files (.npmrc, pip.conf, .netrc, settings.xml) are detected, docksmith auto-generates BuildKit --mount=type=secret instructions and excludes those files from .dockerignore.

Also reads docksmith.yaml and docksmith.json. Run docksmith init to generate a config pre-filled from detection.

Custom framework definitions

Add detection for any framework with a YAML file in ~/.docksmith/frameworks/ or .docksmith/frameworks/ in your project:

name: my-framework
runtime: node
priority: 10

detect:
  all:
    - dependency: my-framework
  any:
    - file: my-framework.config.js

defaults:
  build: "npm run build"
  start: "npm start"

plan:
  port: 3000
  stages:
    - name: builder
      base: node
      steps:
        - workdir: /app
        - copy: ["package*.json", "."]
        - run: "{{install_command}}"
          cache: /root/.npm
        - copy: [".", "."]
        - run: "{{build_command}}"
    - name: runtime
      base: node
      steps:
        - workdir: /app
        - copy_from:
            stage: builder
            src: /app
            dst: .
        - cmd: ["node", "server.js"]

tests:
  - name: detects my-framework
    fixture:
      package.json: '{"dependencies": {"my-framework": "^1.0.0"}}'
    expect:
      detected: true
      framework: my-framework

YAML definitions include inline tests. Run them with docksmith test my-framework.yaml. The community registry (docksmith registry search) provides additional definitions.

Development

make check    # fmt + vet + test + lint (full CI gate)
make test     # tests with race detector
make lint     # golangci-lint
make build    # build CLI to bin/docksmith
make fuzz     # 30s fuzz testing

Requires Go 1.26+ and optionally golangci-lint. The testdata/fixtures/ directory has sample projects for each framework — adding a new framework means adding a detector, a planner, and a fixture.

See CONTRIBUTING.md for full guidelines.

License

Apache License 2.0. See LICENSE.

Documentation

Overview

Package docksmith detects application frameworks and generates production-ready Dockerfiles.

The pipeline has three stages:

fw, err := docksmith.Detect("/path/to/project")
plan := docksmith.Plan(fw)
dockerfile := docksmith.EmitDockerfile(plan)

Each stage is independently useful. Detect analyzes project files to identify the framework and runtime. Plan converts a Framework into abstract build steps. EmitDockerfile serializes a plan into a multi-stage Dockerfile with cache mounts and non-root users.

Detection supports 45 frameworks across 12 runtimes. Custom frameworks can be added via YAML definitions or RegisterDetector.

Index

Constants

View Source
const (
	StepWorkdir     = core.StepWorkdir
	StepCopy        = core.StepCopy
	StepCopyFrom    = core.StepCopyFrom
	StepRun         = core.StepRun
	StepEnv         = core.StepEnv
	StepArg         = core.StepArg
	StepExpose      = core.StepExpose
	StepCmd         = core.StepCmd
	StepEntrypoint  = core.StepEntrypoint
	StepUser        = core.StepUser
	StepHealthcheck = core.StepHealthcheck
)
View Source
const DefaultRegistryURL = registry.DefaultRegistryURL

Registry types

Variables

View Source
var (
	ErrNotDetected   = core.ErrNotDetected
	ErrInvalidConfig = core.ErrInvalidConfig
	ErrInvalidPlan   = core.ErrInvalidPlan
)
View Source
var (
	WithUser                = plan.WithUser
	WithHealthcheck         = plan.WithHealthcheck
	WithHealthcheckDisabled = plan.WithHealthcheckDisabled
	WithRuntimeImage        = plan.WithRuntimeImage
	WithBaseImage           = plan.WithBaseImage
	WithEntrypoint          = plan.WithEntrypoint
	WithExtraEnv            = plan.WithExtraEnv
	WithExpose              = plan.WithExpose
	WithInstallCommand      = plan.WithInstallCommand
	WithBuildCommand        = plan.WithBuildCommand
	WithStartCommand        = plan.WithStartCommand
	WithSystemDeps          = plan.WithSystemDeps
	WithBuildCacheDisabled  = plan.WithBuildCacheDisabled
	WithSecrets             = plan.WithSecrets
	WithContextRoot         = plan.WithContextRoot
)

Plan option constructors

View Source
var ApplySecretMounts = plan.ApplySecretMounts
View Source
var BuildkitCacheArgs = plan.BuildkitCacheArgs
View Source
var CacheDir = plan.CacheDir
View Source
var FetchRegistryIndex = registry.FetchIndex

Registry function aliases

View Source
var FrameworkDefaults = plan.FrameworkDefaults
View Source
var FrameworkFromJSON = core.FrameworkFromJSON
View Source
var InstallFramework = registry.InstallFramework
View Source
var ResolveDockerTag = plan.ResolveDockerTag
View Source
var SearchRegistry = registry.Search
View Source
var SecretBuildHint = plan.SecretBuildHint
View Source
var SecretIgnoreFiles = plan.SecretIgnoreFiles
View Source
var ValidateContextRoot = config.ValidateContextRoot

Functions

func EmitDockerfile

func EmitDockerfile(p *BuildPlan) string

EmitDockerfile serializes a BuildPlan into a Dockerfile string.

func GenerateDockerfile

func GenerateDockerfile(fw *Framework, opts ...PlanOption) (string, error)

GenerateDockerfile runs Plan + EmitDockerfile for the given framework. Returns ("", nil) when fw.Name == "dockerfile" (user already has one).

func GenerateDockerignore

func GenerateDockerignore(fw *Framework) string

GenerateDockerignore returns .dockerignore file content tailored to the framework.

func LoadAndRegisterFrameworks

func LoadAndRegisterFrameworks(dirs ...string) error

LoadAndRegisterFrameworks loads YAML defs from each dir (in order) and registers them as detectors. Resolution order after registration:

  1. .docksmith/frameworks/ in project dir (call with project path first)
  2. ~/.docksmith/frameworks/ (call with home path second)
  3. Built-in Go detectors (already registered at init time)

YAML detectors are prepended, so the last dir passed has lowest YAML priority. Dirs that don't exist are silently skipped.

func NewAutoFetch

func NewAutoFetch(afo AutoFetchOptions) func(dir string) (*core.Framework, error)

NewAutoFetch returns a callback suitable for DetectOptions.AutoFetch. It searches the community registry, installs a matching framework, reloads YAML defs, and re-detects. Returns (nil, nil) on miss or network failure.

func RegisterDetector

func RegisterDetector(name string, d DetectorFunc)

RegisterDetector prepends d to the registry.

func RegisterDetectorBefore

func RegisterDetectorBefore(before, name string, d DetectorFunc)

RegisterDetectorBefore inserts d immediately before the named detector.

func RunFrameworkDefTests

func RunFrameworkDefTests(def *FrameworkDef) error

RunFrameworkDefTests delegates to yamldef.RunFrameworkDefTests.

Types

type AutoFetchOptions

type AutoFetchOptions struct {
	RegistryURL    string
	Interactive    bool
	ConfirmInstall func(name, description string) bool
}

AutoFetchOptions configures the registry auto-fetch callback returned by NewAutoFetch. Pass the result as DetectOptions.AutoFetch.

type BuildConfig

type BuildConfig = config.BuildConfig

type BuildPlan

type BuildPlan = core.BuildPlan

func BuildPlanFromDef

func BuildPlanFromDef(def *FrameworkDef, fw *Framework) (*BuildPlan, error)

BuildPlanFromDef converts a FrameworkDef into a BuildPlan. It resolves Base fields via ResolveDockerTag and converts StepDefs to Steps.

func BuildPlanFromDefDir

func BuildPlanFromDefDir(def *FrameworkDef, dir string) (*BuildPlan, error)

BuildPlanFromDefDir builds a BuildPlan from a FrameworkDef by resolving version and package manager from the given project directory.

func Plan

func Plan(fw *Framework, opts ...PlanOption) (*BuildPlan, error)

Plan converts a detected Framework into a BuildPlan.

type CacheMount

type CacheMount = core.CacheMount

type Config

type Config = config.Config

Config types

func LoadConfig

func LoadConfig(dir string) (*Config, error)

LoadConfig reads the first matching config file from dir. Returns (nil, nil) if no config file exists.

type CopyFrom

type CopyFrom = core.CopyFrom

type CopyFromDef

type CopyFromDef = yamldef.CopyFromDef

type DefaultsDef

type DefaultsDef = yamldef.DefaultsDef

type DetectOptions

type DetectOptions = detect.DetectOptions

type DetectRule

type DetectRule = yamldef.DetectRule

type DetectRules

type DetectRules = yamldef.DetectRules

type DetectorFunc

type DetectorFunc = core.DetectorFunc

type Framework

type Framework = core.Framework

func Build

func Build(dir string, opts ...PlanOption) (string, *Framework, error)

Build runs the full pipeline for dir: detect -> plan -> emit.

func BuildWithOptions

func BuildWithOptions(dir string, detectOpts DetectOptions, planOpts ...PlanOption) (string, *Framework, error)

BuildWithOptions runs the pipeline with custom detection options. When private registry indicators are found, secret mounts are wired into the plan and a build hint comment is prepended to the Dockerfile.

func ConfigToFramework

func ConfigToFramework(c *config.Config) *Framework

ConfigToFramework converts a Config to a Framework for Dockerfile generation.

func Detect

func Detect(dir string) (*Framework, error)

Detect analyzes dir and returns the detected framework.

func DetectWithOptions

func DetectWithOptions(dir string, opts DetectOptions) (*Framework, error)

DetectWithOptions runs detection with custom options.

type FrameworkDef

type FrameworkDef = yamldef.FrameworkDef

YAML definition types

func LoadFrameworkDefs

func LoadFrameworkDefs(dir string) ([]*FrameworkDef, error)

LoadFrameworkDefs loads YAML framework definitions from dir.

type InstallConfig

type InstallConfig = config.InstallConfig

type NamedDetector

type NamedDetector = detect.NamedDetector

Detect types

type PMConfig

type PMConfig = yamldef.PMConfig

type PMSource

type PMSource = yamldef.PMSource

type PlanDef

type PlanDef = yamldef.PlanDef

type PlanOption

type PlanOption = plan.PlanOption

Plan option type

func ConfigToPlanOptions

func ConfigToPlanOptions(c *Config) ([]PlanOption, error)

ConfigToPlanOptions converts Config fields into a PlanOption slice.

func LoadPlanOptions

func LoadPlanOptions(dir string) ([]PlanOption, error)

LoadPlanOptions reads the config from dir and converts it to a PlanOption slice. Returns nil (not an error) when no config file exists.

type RegistryEntry

type RegistryEntry = registry.Entry

type RegistryIndex

type RegistryIndex = registry.Index

type RuntimeCfg

type RuntimeCfg = config.RuntimeCfg

type SecretConfig

type SecretConfig = config.SecretConfig

type SecretDef

type SecretDef = detect.SecretDef

type SecretMount

type SecretMount = core.SecretMount

type Stage

type Stage = core.Stage

type StageDef

type StageDef = yamldef.StageDef

type StartConfig

type StartConfig = config.StartConfig

type Step

type Step = core.Step

type StepDef

type StepDef = yamldef.StepDef

type StepType

type StepType = core.StepType

Plan types

type TestCase

type TestCase = yamldef.TestCase

type TestExpect

type TestExpect = yamldef.TestExpect

type TestResult

type TestResult = yamldef.TestResult

func RunFrameworkTests

func RunFrameworkTests(yamlPath string) ([]TestResult, error)

RunFrameworkTests delegates to yamldef.RunFrameworkTests.

type VersionConfig

type VersionConfig = yamldef.VersionConfig

type VersionSource

type VersionSource = yamldef.VersionSource

Directories

Path Synopsis
cmd
docksmith command
Package core defines the shared types used across all docksmith layers: Framework (detection result), BuildPlan (abstract build steps), Stage, Step, CacheMount, and SecretMount.
Package core defines the shared types used across all docksmith layers: Framework (detection result), BuildPlan (abstract build steps), Stage, Step, CacheMount, and SecretMount.
Package detect identifies application frameworks from project files.
Package detect identifies application frameworks from project files.
Package emit serializes a BuildPlan into a Dockerfile string.
Package emit serializes a BuildPlan into a Dockerfile string.
examples
config-file command
Command config-file shows how to load a docksmith.toml config file and use it to drive Dockerfile generation.
Command config-file shows how to load a docksmith.toml config file and use it to drive Dockerfile generation.
custom-detector command
Command custom-detector shows how to register a custom framework detector.
Command custom-detector shows how to register a custom framework detector.
detect command
Command detect shows how to detect the framework for a project directory.
Command detect shows how to detect the framework for a project directory.
generate command
Command generate detects a project's framework and prints a production Dockerfile.
Command generate detects a project's framework and prints a production Dockerfile.
pipeline command
Command pipeline demonstrates each stage of docksmith separately: detect -> plan -> emit.
Command pipeline demonstrates each stage of docksmith separately: detect -> plan -> emit.
with-overrides command
Command with-overrides shows how to customize Dockerfile generation using PlanOption overrides.
Command with-overrides shows how to customize Dockerfile generation using PlanOption overrides.
Package plan converts a detected Framework into an abstract BuildPlan.
Package plan converts a detected Framework into an abstract BuildPlan.
Package yamldef defines the YAML-driven framework definition schema and evaluation logic for docksmith's extensible detection system.
Package yamldef defines the YAML-driven framework definition schema and evaluation logic for docksmith's extensible detection system.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL