Go library and CLI that detects your framework and generates a hardened, multi-stage Dockerfile. Small dependency surface. No lock-in.
| Type | Go library + CLI |
| Module | github.com/permanu/docksmith |
| Go version | 1.26+ |
| Dependencies | 5 direct external modules |
| 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 | MPL 2.0 |
| Status | Pre-release; MPL-2.0 transition prepared for Permanu use |
$ docksmith detect .
framework: express
port: 3000
node: 22
pm: npm
$ docksmith dockerfile . > Dockerfile
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"]Library (Go projects): go get github.com/permanu/docksmith
CLI:
# Go users — installs from source (needs Go 1.26+)
go install github.com/permanu/docksmith/cmd/docksmith@latest
# Anyone else — pre-built binary (Linux/macOS, amd64/arm64)
curl -fsSL https://raw.githubusercontent.com/permanu/Docksmith/main/install.sh | sh
# Pin a version
curl -fsSL https://raw.githubusercontent.com/permanu/Docksmith/main/install.sh | sh -s -- v0.4.2
# Install to a user-writable dir (no sudo)
INSTALL_DIR=$HOME/.local/bin \
curl -fsSL https://raw.githubusercontent.com/permanu/Docksmith/main/install.sh | shThe script verifies the SHA-256 checksum from the release's checksums.txt. Windows users: use go install or download a .zip from the releases page.
Docker is only needed for docksmith build.
docksmith detect . # what framework is this?
docksmith dockerfile . > Dockerfile # generate it
docker build -t myapp . # build normallyOther 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).
Three stages, each independently usable:
- 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.
- 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). - Emit -- serializes the plan into a Dockerfile string. Standard Dockerfile syntax. No proprietary format, no runtime dependency.
The library keeps a small direct dependency surface: config parsing, registry digest resolution, and optional tracing. No Docker client or container runtime is needed for detection or generation.
The output is a plain Dockerfile. You can commit it, modify it, or throw it away. No lock-in.
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=cachefor 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/staticfor the runtime stage. No shell, no package manager, minimal attack surface. - Health checks -- auto-injected per runtime. Node uses stdlib
http.get, Python usesurllib, Java useswget. Go distroless containers do not get a health check (no shell to run it in).
There are good tools in this space. Here's why I built another one.
Railpack powers all Railway deployments. It builds OCI images directly via BuildKit LLB, which gives it parallel layer execution and graph-level optimization. Railway reports 38% smaller images vs Nixpacks — but that's an LLB advantage, not a base image one. Docksmith uses the same alpine/slim/distroless bases and multi-stage pruning, so final image sizes are comparable. The difference is execution model: Railpack controls the build; docksmith generates a Dockerfile and gets out of the way. If you're on Railway, just use Railpack. If you want a readable Dockerfile you can commit, modify, and build anywhere — that's docksmith.
Nixpacks is in maintenance mode (Railway recommends Railpack), but it covers 23 language providers including Crystal, Haskell, Dart, and Zig. Docksmith doesn't support those. If you need one of those runtimes, Nixpacks is still the answer.
Cloud Native Buildpacks (Paketo) is the most mature option — CNCF project, used by Heroku and Google Cloud. The tradeoff is opacity: you get OCI layers, not a Dockerfile. You can't read, tweak, or commit what it produces. For teams that want full control over their container definition, that's a dealbreaker.
Docksmith generates plain Dockerfiles. That's the whole point. You get a readable file with multi-stage builds, non-root users, tini, distroless bases, health checks, and cache mounts — things most teams know they should add but skip because it's tedious. The output has no runtime dependency on docksmith. Commit it, forget docksmith exists, and your Dockerfile still works.
The library angle matters too: docksmith is a Go library first, CLI second. If you're building a PaaS, a deploy tool, or a CI pipeline that needs framework detection or Dockerfile generation, you can import "github.com/permanu/docksmith" and call three functions. Railpack and Nixpacks can do this too, but Buildpacks are harder to embed.
| 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.
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)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/frontendBuildtime 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.
Add detection for any framework with a YAML file in ~/.docksmith/frameworks/ or .docksmith/frameworks/ in your project.
The simplest possible definition — just detection, no custom build plan:
name: my-framework
runtime: node
detect:
all:
- dependency: my-frameworkThat's it. Docksmith uses the standard Node build plan (deps → build → runtime) with sensible defaults. The dependency rule checks package.json automatically — you don't specify which manifest file.
For full control over the Dockerfile, add a plan with stages. Every field below is optional except name, runtime, and detect:
name: my-framework
runtime: node
# Detection priority — higher wins when multiple definitions match.
# Convention: 1-5 generic runtimes, 10-20 specific frameworks, 50+ niche variants.
priority: 10
detect:
all: # ALL must match (AND):
- dependency: my-framework # package.json lists "my-framework"
any: # AND at least ONE of these (OR):
- file: my-framework.config.js # JS config exists
- file: my-framework.config.ts # or TS config exists
# none: # NONE may match (NOT) — useful for exclusions
defaults:
build: "npm run build"
start: "npm start"
plan:
port: 3000
stages:
- name: builder
base: node # resolved to "node:22-alpine" via ResolveDockerTag
steps:
- workdir: /app
- copy: ["package*.json", "."]
- run: "{{install_command}}" # template vars expanded from defaults + detection
cache: /root/.npm # BuildKit cache mount for this RUN step
- copy: [".", "."]
- run: "{{build_command}}"
- name: runtime
base: node
steps:
- workdir: /app
- copy_from:
stage: builder
src: /app
dst: .
- cmd: ["node", "server.js"]
# Inline tests — run with: docksmith test my-framework.yaml
tests:
- name: detects my-framework
fixture:
package.json: '{"dependencies": {"my-framework": "^1.0.0"}}'
expect:
detected: true
framework: my-frameworkStages map 1:1 to Dockerfile stages — if you can read a multi-stage Dockerfile, you can read this. Template variables ({{build_command}}, {{install_command}}, {{port}}) are expanded from the defaults block and detection results.
The community registry (docksmith registry search) provides additional definitions you can install with docksmith registry install.
Docksmith emits OpenTelemetry spans for every build stage. Spans propagate through any installed TracerProvider; when linked into Permanu, they flow through the unified event bus and render in Permanu's trace viewer.
| Span name | Attributes |
|---|---|
docksmith.build |
docksmith.detected_language, docksmith.dockerfile_path, docksmith.layers_count |
docksmith.classify |
docksmith.detected_language, docksmith.detected_port |
docksmith.plan |
docksmith.detected_language, docksmith.layers_count |
docksmith.cache_check |
docksmith.image_tag, docksmith.digest |
When no TracerProvider is installed (e.g. standalone CLI usage or unit tests), the global OTel default returns a no-op tracer — safe and zero-overhead.
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 testingRequires 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.
Mozilla Public License 2.0. See LICENSE. Docksmith source files are covered by MPL-2.0 even when they do not include per-file license headers; see docs/license-headers.md.
Docksmith is moving from Apache-2.0 to MPL-2.0 so improvements to Docksmith itself stay available while generated Dockerfiles, consuming applications, and host platforms can remain under their own licenses. See docs/license-rationale.md for the rationale and migration note.