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
- Platform file
- Health checks
- 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.2
project: my-first-app
components:
web:
image: nginx:latest
port: 80
environments: [local]
expose:
domain: publicDeployah also needs a platform file, deployah.platform.yaml, that maps
the local environment to a domain and a Kubernetes context. deployah cluster up creates this for you automatically. See Platform file.
deployah 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. - Health checks. Deployah checks that your app is ready for traffic and restarts it if it gets stuck. This happens automatically for every service component. You can improve the checks by giving Deployah an HTTP endpoint to call. See Health checks.
- 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.
Deployah splits configuration across two files, each with a different owner:
deployah.yaml(this section). Owned by the developer. Describes what to run: image, port, resources, health checks, and which logical domain to expose on. It never contains a Kubernetes context or a real domain name.deployah.platform.yaml. Owned by the platform team. Maps each environment to a real Kubernetes context, domain, and TLS strategy. See Platform file.
This split means a developer can add an environment or expose a component without knowing which cluster or domain it runs on, and a platform team can change clusters or rotate certificates without touching the app spec.
Here is a full example that shows the common fields. You do not need all of them; most have defaults.
apiVersion: v1-alpha.2 # 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
expose: # optional: expose over HTTP/HTTPS
domain: public # references a domain key in the platform file
subdomain: api # optional: omit for the domain apex
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 (a map, not a list)
staging:
variables:
TAG: 1.4.0-rc # fills ${TAG} in the image above
prod:
variables:
TAG: 1.4.0 # fills ${TAG} in the image aboveNotice there is no context field here: the Kubernetes context for each
environment comes from deployah.platform.yaml, not from deployah.yaml.
Use 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. Must be v1-alpha.2. |
project |
Yes | Lowercase name (DNS-1123). Prefixes your Kubernetes resources. |
components |
Yes | A map of component name to component settings. |
environments |
Yes in practice | A map of environment name to environment settings. Keys support prefix-based wildcard matching, e.g. a review key matches --environment review/pr-123. |
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). |
expose |
none | domain (required, a key from the platform file) and subdomain (optional; omit for the domain apex). See Platform file. |
autoscaling |
off | enabled, minReplicas, maxReplicas, metrics. |
health |
auto | Ready and alive checks. See Health checks. |
environments |
none | Which environments deploy this component. |
profile |
none | Platform profile name. Parsed but not enforced yet. |
Important
Not deployed yet: the schema accepts role: worker and role: job,
kind: stateful, and the env, envFile, configFile, and profile
fields, but Deployah does not apply them at deploy time yet. Today, deploy a
stateless service using image, port, resources or
resourcePreset, expose, and autoscaling.
Environment:
| Field | Notes |
|---|---|
envFile / configFile |
Files to load for this environment (see below). |
variables |
Values for ${...} placeholders in your spec. |
There is no context field on an environment: it comes from the matching
environment key in deployah.platform.yaml.
To check your spec without a platform file, run deployah validate. To also
check it resolves against the platform file for a given environment, 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.expose.domain: a key that must exist in the target environment'sdomainsmap in the platform file.expose.subdomain: a DNS-1123 label, likeapiorwww. Omit it to expose the component at the domain apex.autoscaling: needsenabled,minReplicas, andmaxReplicas. Each metric has atype(cpuormemory) and atargetpercentage.health.alive.intervalandhealth.alive.restartAfter: a positive integer followed by a unit:s(seconds),m(minutes), orh(hours). For example10s,2m,1h. The effective restart time rounds up to the nearest multiple ofinterval.- 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.2
project: hello
components:
web:
image: nginx:latest
environments: [dev]
environments:
dev: {}Two components. A web app and an API in one project.
apiVersion: v1-alpha.2
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:
prod: {}Several environments. Each one has its own image tag. The cluster comes from the platform file, not from here.
apiVersion: v1-alpha.2
project: shop
components:
web:
image: ghcr.io/acme/web:${TAG}
port: 80
environments: [staging, prod]
environments:
staging:
variables:
TAG: 1.0.0-rc
prod:
variables:
TAG: 1.0.0Expose it over HTTPS. Add an expose block. TLS is decided by the
platform file's domain configuration, not by the developer.
apiVersion: v1-alpha.2
project: shop
components:
web:
image: ghcr.io/acme/web:1.0.0
port: 80
environments: [prod]
expose:
domain: public
subdomain: shop
environments:
prod: {}Set exact resources. Use resources instead of a preset.
apiVersion: v1-alpha.2
project: shop
components:
web:
image: ghcr.io/acme/web:1.0.0
port: 80
environments: [prod]
resources:
cpu: 500m
memory: 512Mi
environments:
prod: {}Autoscale on CPU. Scale between 2 and 6 replicas at 70% CPU.
apiVersion: v1-alpha.2
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:
prod: {}Any component that uses expose needs a second file, deployah.platform.yaml,
next to deployah.yaml. It maps each environment to a real Kubernetes context,
one or more domains, and a TLS strategy. This file is not processed with
${...} substitution: it holds real values, not templates.
apiVersion: platform/v1-alpha.1
environments:
production:
context: prod-eks
domains:
public:
baseDomain: example.com
tls:
mode: certManager
issuer: letsencrypt-prod
local:
context: kind-deployah
domains:
public:
baseDomain: 127.0.0.1.nip.io
tls:
mode: selfSignedA component's expose.domain: public resolves against the public key under
the active environment. expose.subdomain: api combines with baseDomain to
form api.example.com; omitting subdomain exposes the component at the
domain apex (example.com).
| Mode | Meaning |
|---|---|
selfSigned |
Deployah generates and manages a self-signed certificate. Used by the local cluster. |
secretName |
Use a pre-existing Kubernetes TLS secret in the target namespace. Set secretName to its name. |
certManager |
Provision the certificate through cert-manager. Set issuer to a ClusterIssuer or Issuer name. |
deployah initscaffolds bothdeployah.yamlanddeployah.platform.yaml.deployah cluster upcreates or updatesdeployah.platform.yamlwith alocalenvironment pointed at the local cluster.- Deployah looks for the platform file in this order:
--platform-file, theDEPLOYAH_PLATFORM_FILEenvironment variable, then the same directory as the spec file.
If a component uses expose and no platform file can be found, deployah deploy and deployah validate <environment> stop with an error rather than
guessing. Use deployah resolve <environment> to preview the fully resolved
hostname, TLS mode, and context without touching a cluster:
deployah resolve production
deployah resolve production --output jsonOnce a component has been deployed with a resolved hostname, changing the
domain or subdomain on the next deploy is blocked by default, since it can
silently drop traffic. Pass --force-hostname-change to deployah deploy to
allow it.
Deployah checks that your app is running and ready for traffic. For every
service component with a port, Deployah adds three checks automatically:
- Startup check. Waits up to 3 minutes for your app to accept connections on its port. New pods do not receive traffic until this passes. If the app takes longer than 3 minutes to start, the pod is killed and restarted.
- Ready check. Runs every 5 seconds. If your app stops accepting connections for 15 seconds, traffic is routed to other pods until it recovers.
- Alive check. Runs every 10 seconds. If your app is unresponsive for 60 seconds, the pod is restarted.
With no configuration, all three checks connect to your app's port (TCP). This works for any app. You can make the checks smarter by giving Deployah an HTTP endpoint to call.
Zero config. All checks run automatically. No health block needed.
components:
api:
image: my-app:1.0.0
port: 8080Add a readiness endpoint. Tell Deployah where to check if your app is ready for traffic. This also upgrades the startup check to the same endpoint.
components:
api:
image: my-app:1.0.0
port: 8080
health:
ready:
path: /healthYour /health endpoint should return a 2xx status code when your app can
handle requests. Return 4xx or 5xx when it cannot, for example if it is
still connecting to the database.
Add a separate restart endpoint. If your app can get stuck in a way that a restart fixes, give Deployah a separate endpoint to check. If this endpoint fails for long enough, the pod is restarted.
components:
api:
image: my-app:1.0.0
port: 8080
health:
ready:
path: /health
alive:
path: /livez
interval: 10s # how often to check (default: 10s)
restartAfter: 60s # how long to fail before restart (default: 60s)Your /livez endpoint should check only whether the process itself is
responsive. Do not check external dependencies (databases, caches) here. If a
dependency is down, let the ready endpoint return an error instead. That stops
traffic without restarting the pod.
Disable checks. For a raw TCP service or an app where checks cause problems, you can disable them individually.
components:
game-server:
image: my-game:1.0.0
port: 9000
health:
ready: false
alive: falseRun 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). |
--platform-file |
Path to the platform config file (overrides DEPLOYAH_PLATFORM_FILE and the default same-directory lookup). |
|
--namespace |
-n |
Kubernetes namespace to use. |
--context |
Kubernetes context to use (overrides the platform file'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 and platform file by answering a few questions. Use -o to set the output file, or --dry-run to preview. |
deployah validate |
Check the manifest schema only (offline, no platform file needed). |
deployah validate <environment> |
Also load the platform file and check the resolved configuration for that environment. |
deployah resolve <environment> |
Preview the fully resolved hostname, TLS mode, and context, offline. Use --output json for machine-readable output. |
deployah deploy <environment> |
Deploy your project. Use --dry-run to render without installing, --explain to print the resolution report first, or --force-hostname-change to bypass the hostname guard. |
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. Fails if no platform file is found, unless you pass --allow-missing-platform. Use --force to skip the prompt, or --show-resources to preview. |
| Command | What it does |
|---|---|
deployah cluster up |
Create the local cluster, start the cloud provider, and create or update deployah.platform.yaml with a local environment. |
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:
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 or HTTPS, give it an expose block and add a
matching domain to your platform file's local environment:
# deployah.yaml
components:
web:
image: nginx:latest
port: 80
environments: [local]
expose:
domain: public# deployah.platform.yaml (created for you by 'deployah cluster up')
environments:
local:
context: kind-deployah
domains:
public:
baseDomain: 127.0.0.1.nip.io
tls:
mode: selfSignedOn the local cluster, run deployah cluster status to see the resolved URL
and port for your app. Open that URL in your browser; nip.io resolves to
127.0.0.1 for you, so you do not need extra setup or /etc/hosts entries.
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
map. Run deployah validate 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
deployed.
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).
Symptom. deployah deploy completes, the pod is Running and 1/1, but
requests to the app fail:
< HTTP/1.1 503 Service Unavailable
< server: envoy
upstream connect error or disconnect/reset before headers. reset reason: connection timeout
kubectl port-forward svc/<project>-<env>-web 8080:80 works, which confirms
the pod is healthy and only the ingress path is broken.
Cause. The local cluster uses cloud-provider-kind to serve ingress. Its Envoy gateway runs in a container and forwards traffic from the container network into the cluster. When the host drops that forwarded traffic, Envoy returns 503 -- even though the pod is fine. This is a host networking issue, not a Deployah or app problem.
The host's iptables FORWARD chain defaults to DROP (set by Docker, or
re-imposed by firewalld/ufw), which silently drops the gateway's traffic.
Confirm:
sudo iptables -S FORWARD | head -1
# -P FORWARD DROP <-- this is the causeFind the Kind bridge interface:
bridge=br-$(docker network inspect kind -f '{{.Id}}' | cut -c1-12)Then apply one of these fixes:
firewalld:
sudo firewall-cmd --permanent --zone=trusted --change-interface="$bridge"
sudo firewall-cmd --reloadiptables / nftables (survives Docker restarts without opening the whole host):
sudo iptables -I DOCKER-USER -o "$bridge" -j ACCEPT
sudo iptables -I DOCKER-USER -i "$bridge" -j ACCEPTufw: set DEFAULT_FORWARD_POLICY="ACCEPT" in /etc/default/ufw, then run
sudo ufw reload.
Re-run your request afterwards. Avoid sudo iptables -P FORWARD ACCEPT -- it
works but opens the entire host to forwarded traffic.
The Docker daemon runs inside a Linux VM, so there is no host firewall rule to change. Instead:
- Always reach the app via
127.0.0.1, never a172.xcontainer address. Deployah publishes ingress on127.0.0.1by default. - Recreate the cluster:
deployah cluster down && deployah cluster up. - Restart the VM: quit and reopen Docker Desktop, or
orb restart, orpodman machine stop && podman machine start. - If the problem persists, see the upstream issue.
kubectl --kubeconfig "$(deployah cluster kubeconfig)" \
port-forward svc/<project>-<env>-web 8080:80
curl http://localhost:8080Services 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 and platform file with JSON Schema.
- Manifest schema version: v1-alpha.2
- Manifest schema:
internal/spec/schema/v1-alpha.2/manifest.json - Manifest environments schema:
internal/spec/schema/v1-alpha.2/environments.json - Platform schema version: platform/v1-alpha.1
- Platform schema:
internal/spec/schema/platform/v1-alpha.1/platform.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.