Warning
Deployah is experimental and under active development.
Deployah is a CLI tool that makes deploying apps easy. It uses Helm under the
hood, so you do not need to know Kubernetes or Helm, or even install the helm,
kubectl, or kind tools. It is a single binary.
You write a short spec. Deployah turns it into a running release on Kubernetes. We call this Spec-to-Release. It is like Source-to-Image (S2I), but for the deploy step: S2I builds your image, and Deployah runs your release.
- Installation
- Requirements
- Quick start
- How Deployah works
- Concepts
- Writing your spec
- Commands
- Environments and variables
- Accessing your app
- Local cluster networking
- Troubleshooting
- Schema reference
- Development
brew install deployah-dev/tap/deployahIf you have Nix installed:
# Run without installing
nix run github:deployah-dev/deployah
# Or add it to your flake.nix
inputs.deployah.url = "github:deployah-dev/deployah";go install deployah.dev/deployah@latestDeployah is a single binary. You do not need the helm, kubectl, or kind
command-line tools. Deployah includes Helm, the Kubernetes client, and Kind as
libraries, so it talks to your cluster by itself.
- Deploy to a cluster you already have: you only need access to it (a kubeconfig). No container runtime is required.
- Use the built-in local cluster (
deployah cluster up): you need a container runtime, either Docker or Podman. This is the only extra tool, and it is needed only for the local cluster.
This walks you through one full deploy on your own machine. It takes about five minutes. For the local cluster you need Docker or Podman running (see Requirements).
You do not need an existing Kubernetes cluster. Deployah can make a local one for you.
deployah cluster upThis creates a small local Kubernetes cluster (using Kind) and gives it the
context name kind-deployah.
Save this as deployah.yaml in an empty folder. It runs the public nginx
image, so you do not need to build anything.
apiVersion: v1-alpha.1
project: my-first-app
components:
web:
image: nginx:latest
port: 80
environments: [local]
ingress:
host: my-first-app.local
environments:
- name: local
context: kind-deployahdeployah deploy local# Show the status of your project
deployah status my-first-app
# Show the local cluster and the URLs you can open
deployah cluster statusdeployah cluster status prints a ready-to-use URL for your app. Open it in
your browser to see the nginx welcome page.
You can also stream the logs:
deployah logs my-first-app# Remove the app
deployah delete my-first-app local
# Stop and delete the local cluster
deployah cluster downDeployah turns your deployah.yaml spec into a running Kubernetes deployment in
three steps.
flowchart LR
subgraph phase1["1. Read the spec"]
direction TB
A["YAML spec"] --> B["Parse"] --> C["Validate"]
end
subgraph phase2["2. Resolve config"]
direction TB
D["Pick environment"] --> E["Apply variables"] --> F["Fill defaults"]
end
subgraph phase3["3. Deploy"]
direction TB
G["Build Helm values"] --> H["Install release"]
end
phase1 --> phase2 --> phase3
- Read the spec. Deployah reads your
deployah.yamland checks it against a JSON Schema, so mistakes are caught early with clear messages. - Resolve config. Deployah picks the environment you asked for, substitutes your variables, and fills in sensible defaults.
- Deploy. Deployah builds Helm values from your spec and installs a Helm release on your cluster. You never write a Helm chart yourself.
For how Deployah compares to similar tools (DevSpace, Werf, Score, Epinio, Kubero), see docs/comparison.md.
A few words you will see often.
- Project. One app, with a name. The name prefixes the Kubernetes resources
Deployah creates. It is the
projectfield in your spec. - Component. One deployable part of your project, such as a web service or a background worker. A project has one or more components.
- Role. What a component is for:
service: it serves traffic and can be exposed (the default).worker: a long-running background task, not exposed.job: a one-off task that runs and then stops.
- Kind.
stateless(the default, easy to scale) orstateful(needs persistent storage). - What deploys today. Deployah currently deploys
statelessservicecomponents. Theworkerandjobroles and thestatefulkind are in the schema but are not deployable yet, so a deploy that uses them stops with a "not supported yet" error. - Environment. A target such as
dev,staging, orprod. Each environment can use a different cluster, different files, and different variables. - Resource preset. A quick way to set CPU and memory without knowing
Kubernetes units. Use
resourcePreset: smallinstead of writing exact values. - Bring your own image. Deployah does not build images. You give it an image that already exists in a registry your cluster can pull from. Build your image in CI (or locally), then let Deployah deploy it.
Your spec is a file named deployah.yaml. It has three required parts:
apiVersion, project, and components. You also define your environments.
Here is a full example that shows the common fields. You do not need all of them; most have defaults.
apiVersion: v1-alpha.1 # required: the schema version
project: shop # required: your project name
components: # required: one or more components
api:
image: ghcr.io/acme/shop-api:${TAG} # tag comes from the environment below
role: service # service | worker | job (default: service)
kind: stateless # stateless | stateful (default: stateless)
port: 8080 # the port your app listens on (default: 8080)
environments: [staging, prod] # which environments deploy this component
command: ["/bin/api"] # optional: override the image ENTRYPOINT
args: ["--verbose"] # optional: override the image CMD
env: # planned: not applied to the container yet
LOG_LEVEL: info
resourcePreset: small # nano|micro|small|medium|large|xlarge|2xlarge
ingress: # optional: expose over HTTP/HTTPS
host: api.example.com # required when you use ingress
tls: false # default: false
autoscaling: # optional: scale on CPU or memory
enabled: true
minReplicas: 2
maxReplicas: 5
metrics:
- type: cpu # cpu | memory
target: 70 # target usage percentage
environments: # define your environments
- name: staging
context: kind-deployah # the kube context to deploy to
variables:
TAG: 1.4.0-rc # fills ${TAG} in the image above
- name: prod
context: prod-cluster
variables:
TAG: 1.4.0 # fills ${TAG} in the image aboveUse either resourcePreset or resources, not both. Presets are the easy
option; resources lets you set exact CPU, memory, and ephemeral storage.
Top level:
| Field | Required | Notes |
|---|---|---|
apiVersion |
Yes | The schema version, for example v1-alpha.1. |
project |
Yes | Lowercase name (DNS-1123). Prefixes your Kubernetes resources. |
components |
Yes | A map of component name to component settings. |
environments |
Yes in practice | The list of environments you can deploy to. |
Component:
| Field | Default | Notes |
|---|---|---|
image |
none | The container image to run. You provide this. |
role |
service |
service, worker, or job. |
kind |
stateless |
stateless or stateful. |
port |
8080 |
The port your app listens on (1 to 65535). |
command / args |
none | Override the image ENTRYPOINT and CMD. |
env |
none | Environment variables (uppercase keys). |
resourcePreset |
none | nano, micro, small, medium, large, xlarge, 2xlarge. |
resources |
none | cpu, memory, ephemeralStorage (Kubernetes units). |
ingress |
none | host (required) and tls (default false). |
autoscaling |
off | enabled, minReplicas, maxReplicas, metrics. |
environments |
none | Which environments deploy this component. |
Important
Not deployed yet: the schema accepts role: worker and role: job,
kind: stateful, and the env, envFile, and configFile fields, but
Deployah does not apply them at deploy time yet. Today, deploy a stateless
service using image, port, resources or resourcePreset, ingress,
and autoscaling.
Environment:
| Field | Notes |
|---|---|
name |
The environment name. Must be unique. |
context |
The Kubernetes context to use. For the local cluster, use kind-deployah. |
variables |
Values for ${...} placeholders in your spec. |
envFile / configFile |
Files to load for this environment (see below). |
To check your spec without deploying, run deployah validate <environment>.
A few fields have specific formats:
port: a number from 1 to 65535.resources.cpu: millicores like500m, or whole cores like1or2.resources.memoryandresources.ephemeralStorage: a number with a unit, like256Mior1Gi.env: keys are uppercase letters, digits, and underscores, and start with a letter or underscore (for exampleLOG_LEVEL). Values are a string, number, or boolean.ingress.host: a domain name with at least one dot, likeapi.example.comormy-app.local.autoscaling: needsenabled,minReplicas, andmaxReplicas. Each metric has atype(cpuormemory) and atargetpercentage.- Names (
project, component names, environment names): letters, digits,-, and_. An environment name must be at least 2 characters.
A preset sets CPU and memory for you, so you do not need to know Kubernetes
units. Use resourcePreset: <name> on a component instead of writing resources.
These are the current values (request / limit):
| Preset | CPU (request / limit) | Memory (request / limit) |
|---|---|---|
nano |
100m / 150m | 128Mi / 192Mi |
micro |
250m / 375m | 256Mi / 384Mi |
small |
500m / 750m | 512Mi / 768Mi |
medium |
500m / 750m | 1024Mi / 1536Mi |
large |
1000m / 1500m | 2048Mi / 3072Mi |
xlarge |
1000m / 3000m | 3072Mi / 6144Mi |
2xlarge |
1000m / 6000m | 3072Mi / 12288Mi |
All presets use the same ephemeral storage: 50Mi request, 2Gi limit.
Every example below is complete and valid. Copy one and change the values.
Smallest spec. One service, one environment.
apiVersion: v1-alpha.1
project: hello
components:
web:
image: nginx:latest
environments: [dev]
environments:
- name: devTwo components. A web app and an API in one project.
apiVersion: v1-alpha.1
project: shop
components:
web:
image: ghcr.io/acme/web:1.0.0
port: 80
environments: [prod]
api:
image: ghcr.io/acme/api:1.0.0
port: 8080
environments: [prod]
environments:
- name: prodSeveral environments. Each one has its own cluster and its own image tag.
apiVersion: v1-alpha.1
project: shop
components:
web:
image: ghcr.io/acme/web:${TAG}
port: 80
environments: [staging, prod]
environments:
- name: staging
context: kind-deployah
variables:
TAG: 1.0.0-rc
- name: prod
context: prod-cluster
variables:
TAG: 1.0.0Expose it over HTTPS. Add an ingress with a host and TLS.
apiVersion: v1-alpha.1
project: shop
components:
web:
image: ghcr.io/acme/web:1.0.0
port: 80
environments: [prod]
ingress:
host: shop.example.com
tls: true
environments:
- name: prodSet exact resources. Use resources instead of a preset.
apiVersion: v1-alpha.1
project: shop
components:
web:
image: ghcr.io/acme/web:1.0.0
port: 80
environments: [prod]
resources:
cpu: 500m
memory: 512Mi
environments:
- name: prodAutoscale on CPU. Scale between 2 and 6 replicas at 70% CPU.
apiVersion: v1-alpha.1
project: shop
components:
web:
image: ghcr.io/acme/web:1.0.0
port: 80
environments: [prod]
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 6
metrics:
- type: cpu
target: 70
environments:
- name: prodRun deployah <command> --help for the full details of any command. A complete,
generated reference for every command and flag is in
docs/cli/.
Deployah can also generate a shell completion script: run deployah completion
(use -o to write it to a file). See deployah completion --help for details.
These work with every command:
| Flag | Short | Meaning |
|---|---|---|
--spec |
-s |
Path to the spec file (default: deployah.yaml). |
--namespace |
-n |
Kubernetes namespace to use. |
--context |
Kubernetes context to use (overrides the environment's context). | |
--kubeconfig |
-k |
Path to your kubeconfig file. |
--timeout |
-t |
Timeout for operations (default: 10m). |
--debug |
-d |
Verbose logging, and keep temporary files. |
| Command | What it does |
|---|---|
deployah init |
Create a new spec by answering a few questions. Use -o to set the output file, or --dry-run to preview. |
deployah validate <environment> |
Check your spec against the schema. |
deployah deploy <environment> |
Deploy your project. Use --dry-run to render without installing. |
deployah status <project> |
Show the status of a deployed project. Use --detailed for pod details, -e for an environment. |
deployah logs <project> |
Stream logs. Filter with --component, -e, --container, --since, --tail. Use --no-follow for a one-off read. |
deployah shell <project> |
Open a shell in a running container. Choose with --component and --container. |
deployah list |
List deployed projects. Filter with -p (project) and -e (environment). |
deployah delete <project> <environment> |
Remove a deployment. Use --force to skip the prompt, or --show-resources to preview. |
| Command | What it does |
|---|---|
deployah cluster up |
Create the local cluster and start the cloud provider. |
deployah cluster status |
Show the cluster status and the URLs you can open. |
deployah cluster down |
Delete the local cluster. Use --force to skip the prompt. |
deployah cluster kubeconfig |
Print the local cluster kubeconfig path. Use --raw for its contents. |
Deployah supports multiple environments (for example dev, staging, prod).
You define them in the environments list, and you choose one when you deploy:
deployah deploy stagingIf you define more than one environment, you must say which one to use. If you do not, Deployah shows an error that lists the environments you can pick.
It helps to know there are two different things:
- Substitution variables. These fill
${...}placeholders in your spec before Deployah reads it. Use them to change the spec itself, such as the image tag or the ingress host. This works today and is described below. - Container environment variables. These are the variables your app reads
at runtime. You would set them with the
envfield on a component. Note: that field is accepted by the schema but is not applied to the running container yet (it is planned). For now, put runtime values into your image or your app's own config.
You can use ${NAME} placeholders anywhere in your spec. Two forms are
supported:
${NAME}is required. If the variable is not set, Deployah stops with an error ("variable not set"). This stops you from deploying with a missing value.${NAME:-default}usesdefaultwhen the variable is not set.
For example:
components:
web:
image: nginx:${TAG:-latest} # uses "latest" when TAG is not set
port: 80
environments: [prod]Deployah uses fluxcd/pkg/envsubst under the hood, so more shell-style forms work too. The full list is below.
These forms come from
fluxcd/pkg/envsubst.
In the table, var is your variable name.
| Expression | Meaning |
|---|---|
${var} |
The value of var. |
${#var} |
The length of var. |
${var^} |
Uppercase the first character. |
${var^^} |
Uppercase all characters. |
${var,} |
Lowercase the first character. |
${var,,} |
Lowercase all characters. |
${var:n} |
Start n characters in. |
${var:n:len} |
Start n characters in, take up to len characters. |
${var#pattern} |
Remove the shortest pattern match from the start. |
${var##pattern} |
Remove the longest pattern match from the start. |
${var%pattern} |
Remove the shortest pattern match from the end. |
${var%%pattern} |
Remove the longest pattern match from the end. |
${var-default} |
Use default if var is not set. |
${var:-default} |
Use default if var is not set or is empty. |
${var=default} |
Use default if var is not set. |
${var:=default} |
Use default if var is not set or is empty. |
${var/pattern/replacement} |
Replace the first pattern match with replacement. |
${var//pattern/replacement} |
Replace every pattern match with replacement. |
${var/#pattern/replacement} |
Replace a pattern match at the start with replacement. |
${var/%pattern/replacement} |
Replace a pattern match at the end with replacement. |
Remember: Deployah runs in strict mode. A variable with no default must be set, or the deploy stops with an error.
Deployah looks for a variable in three places. If the same name is set in more than one place, the later one wins (lowest to highest):
- The environment's
variablesin your spec. Write these with their plain name, with no prefix. - The environment's env file, for example
.env.production. Only keys that start withDPY_VAR_are used, and the prefix is removed. - Your shell, also with the
DPY_VAR_prefix.
So the same ${APP_ENV} can come from any of these:
# in deployah.yaml (no prefix here)
environments:
- name: production
variables:
APP_ENV: from-spec# in .env.production (needs the prefix)
DPY_VAR_APP_ENV=from-envfile# in your shell (needs the prefix)
export DPY_VAR_APP_ENV=from-shellWith all three set, ${APP_ENV} is from-shell, because the shell wins.
Note
Only env-file and shell variables need the DPY_VAR_ prefix, because
Deployah has to pick its own variables out of all the others on your system.
The variables you write inside the spec do not need a prefix.
An env file is a simple list of KEY=value lines. Blank lines and lines that
start with # are ignored, and spaces around the key and value are trimmed.
If you do not set envFile for an environment, Deployah looks for a file in
this order and uses the first one it finds:
.env.<environment>(for example.env.production).deployah/.env.<environment>.env.deployah/.env
If you do set envFile and the file is missing, Deployah stops with an error.
| File | Used by | Purpose |
|---|---|---|
deployah.yaml |
Deployah | Your spec. |
.env / .env.<env> |
Deployah and your app | Variables. Deployah only reads the keys that start with DPY_VAR_. |
config.yaml / config.<env>.yaml |
Your app | Your app's own config. Deployah ignores these. |
Keys in an env file that do not start with DPY_VAR_ are left alone. Deployah
does not use them, so they are free for your app to read on its own. The config
files are for your app only.
To reach a component over HTTP, give it an ingress with a host:
components:
web:
image: nginx:latest
port: 80
environments: [local]
ingress:
host: my-app.localOn the local cluster, run deployah cluster status to see the URL and port for
your app. Open that URL in your browser. The local cluster maps it to
localhost for you, so you do not need extra setup.
The local cluster runs Kind (Kubernetes in Docker) with cloud-provider-kind for LoadBalancer, Ingress, and Gateway API support.
On Linux and macOS, services are reachable on localhost through Docker port
mapping. The path traffic takes is:
localhost:<port>
-> Docker port mapping
-> Envoy gateway container
-> Kind cluster pod
On macOS and other Docker-in-VM setups (Lima, Colima, Docker Desktop, OrbStack), there is one more layer. Your Docker runtime forwards the VM port to the host automatically, so you do not configure anything:
macOS localhost:<port>
-> VM port forwarding (automatic)
-> Docker port mapping
-> Envoy gateway container
-> Kind cluster pod
Note
LoadBalancer, Ingress, and Gateway API need a rootful Docker daemon.
Rootless Docker cannot mount the Docker socket into the cloud-provider-kind
container, so it cannot manage LoadBalancer resources.
Run deployah cluster status at any time to see the assigned ports and URLs for
all Ingress and LoadBalancer resources.
Spec is missing a required field.
error: load spec: ... spec is missing 'apiVersion' fieldYour spec needs apiVersion, project, and components, and an environments
list. Run deployah validate <environment> to find the problem.
Environment not found.
error: environment "production" not foundCheck the environment name in your spec, or run deployah list to see what is
available.
Variable not found.
error: variable ${IMAGE} not foundDefine the variable in the environment's variables, or in your env file or
shell with the DPY_VAR_ prefix.
Cannot connect to Kubernetes.
error: unable to connect to Kubernetes clusterCheck that your cluster is reachable with kubectl cluster-info. For a local
cluster, run deployah cluster up and deploy with the local environment (or
pass --context kind-deployah).
Services return "Empty reply from server" on macOS (Lima).
Lima's VZ driver uses a usernet port forwarder by default, which has a known issue with the custom Docker network that Kind creates. To fix it, edit your Lima config:
limactl stop <instance>
limactl edit <instance>Make sure both settings are present at the top level:
ssh:
overVsock: false
portForwards:
- guestIPMustBeZero: true
guestPortRange: [1, 65535]
hostIP: 127.0.0.1
- guestSocket: "/var/run/docker.sock"
hostSocket: "{{.Dir}}/sock/docker.sock"Then restart:
limactl start <instance>ssh.overVsock: false switches Lima to the standard SSH port forwarder. The
portForwards rule forwards all guest ports to the host, which is needed for
the dynamic Docker ports.
"permission denied" in cloud-provider-kind logs.
The cloud provider needs a rootful Docker daemon. If you use Lima, create a rootful instance:
limactl start template:docker-rootfulFirewall blocks gateway ports.
Gateway ports are bound on all interfaces (0.0.0.0). On Linux, allow the
mapped ports in your firewall. On macOS, the Application Firewall may ask for
permission. Allow it when prompted.
deployah --help
deployah <command> --helpDeployah validates your spec with JSON Schema. The schema defines the structure
and the rules for deployah.yaml.
- Schema version: v1-alpha.1
- Spec schema:
internal/spec/schema/v1-alpha.1/manifest.json - Environments schema:
internal/spec/schema/v1-alpha.1/environments.json
For the latest schema and examples, see the schema directory in the repository.
The Nix flake is the main dev and CI interface. With
direnv (the .envrc uses use flake), the tools load
automatically when you enter the repo.
nix developnix run .#fmt # format Go (gofumpt + gci)
nix run .#lint # golangci-lint
nix run .#lint-md # markdownlint
nix run .#tidy # go mod tidy
nix run .#update-vendor-hash # refresh vendorHash after go.sum changesUnit and integration tests are split by build tag. Plain go test ./... skips
the integration tests.
nix run .#test-unit # unit tests with the race detector
nix run .#test-integration # scenario tests in internal/testingCoverage profiles are written to coverage-unit.out and
coverage-integration.out.
nix build # build the deployah binary
nix run . -- --help # run without installingnix flake check # runs the pre-commit hooks (lint, markdownlint, tidy, nixfmt)Format Nix files with nix fmt.