Skip to content

Installation

This guide covers deploying Dploy to a Kubernetes cluster.

  • Kubernetes cluster (1.30+)
  • Flux controllers installed — Dploy uses the source-controller and helm-controller (you do not need the full Flux GitOps setup, just those two controllers and their CRDs)
  • OIDC provider (Authentik, Keycloak, Dex, …) for JWT authentication
  • Ingress controller or Gateway API controller to expose environments
  • kubectl and Helm 3 configured against your cluster

If you don’t already run Flux, the source and helm controllers are enough:

Terminal window
flux install --components=source-controller,helm-controller

Dploy itself is one HTTP service that needs to be reachable at a single URL (e.g. dploy.example.com). Per-instance environments need a hostname per instance (<name>-<uid>.<baseDomain>), routed to the workload Service the chart creates.

Either routing technology works for both layers — and you can mix them (e.g. Gateway API for the per-instance traffic, Ingress for the dploy API). Pick what your cluster already runs.

Option A — Ingress controller (nginx, traefik, cilium-ingress, …)

Section titled “Option A — Ingress controller (nginx, traefik, cilium-ingress, …)”

The chart’s ingress.* values render an Ingress for the dploy API directly:

ingress:
enabled: true
className: nginx
host: dploy.example.com
annotations:
# If your ingress controller doesn't set per-Ingress addresses (e.g. cilium
# shared LB), pin external-dns to the shared LB IP yourself.
external-dns.alpha.kubernetes.io/target: "10.0.0.42"
tls:
- secretName: dploy-tls
hosts: [dploy.example.com]

Per-instance routing is set in each DployTemplate.valuesTemplate — most community charts expose ingress.*:

valuesTemplate: |
ingress:
enabled: true
className: nginx
hosts:
- host: "{{ .Host }}"
paths: [{ path: /, pathType: Prefix }]

external-dns picks up Ingress out of the box with --source=ingress.

Option B — Gateway API (cilium agentgateway, envoy-gateway, istio, …)

Section titled “Option B — Gateway API (cilium agentgateway, envoy-gateway, istio, …)”

Deploy a Gateway once (cluster-wide or per-namespace), then dploy and every instance attaches as an HTTPRoute:

# Cluster Gateway — owned outside the dploy chart.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: dploy-gateway
namespace: gateway-system
spec:
gatewayClassName: agentgateway # or envoy, istio, …
listeners:
- { name: http, port: 80, protocol: HTTP, allowedRoutes: { namespaces: { from: All } } }

Dploy chart doesn’t render an HTTPRoute today; if you want the dploy API on Gateway API, drop a route in next to it:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: dploy, namespace: dploy-system }
spec:
parentRefs: [{ name: dploy-gateway, namespace: gateway-system }]
hostnames: [dploy.example.com]
rules:
- matches: [{ path: { type: PathPrefix, value: / } }]
backendRefs: [{ name: dploy, port: 80 }]

Per-instance routing again lives in valuesTemplate — charts in AYDEV-FR/dploy-charts expose httpRoute.*:

valuesTemplate: |
httpRoute:
enabled: true
parentRefs:
- { name: dploy-gateway, namespace: gateway-system }
hostnames: ["{{ .Host }}"]
annotations:
external-dns.alpha.kubernetes.io/target: "10.0.0.42"

external-dns ≥ 0.14 supports the Gateway API but the source is opt-in and requires an extra RBAC verb the default chart doesn’t grant. Patch both:

Terminal window
# 1. Enable the source.
kubectl patch deploy -n dns-system external-dns --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--source=gateway-httproute"}]'
# 2. Grant list/watch on namespaces (the gateway-httproute source needs it
# for cross-namespace parentRef resolution; without it the pod crash-loops
# with `failed to sync *v1.Namespace: context deadline exceeded`).
kubectl patch clusterrole external-dns --type=json \
-p='[{"op":"add","path":"/rules/-","value":{"apiGroups":[""],"resources":["namespaces"],"verbs":["get","list","watch"]}}]'

The DNS target comes from the parent Gateway’s status.addresses by default. Override with external-dns.alpha.kubernetes.io/target on the HTTPRoute itself when the gateway has no address (e.g. it’s behind a separate LB), as shown in the valuesTemplate snippet above.

Common pattern: dploy itself on a stable Ingress (often pre-existing), per-instance environments on the Gateway API. Both work concurrently; external-dns just needs both --source=ingress (default) and --source=gateway-httproute (opt-in).

The chart deploys both components (operator + API) and installs the CRDs.

Terminal window
helm install dploy ./charts/dploy \
--namespace dploy-system \
--create-namespace \
--set auth.jwksURL="https://your-oidc-provider.com/keys" \
--set auth.jwtIssuer="https://your-oidc-provider.com" \
--set auth.oidcClientID="dploy" \
--set auth.oidcClientSecret="your-client-secret"
values.yaml
config:
namespace: dploy-system # where DployTemplate/DployInstance CRs live
defaultTTL: 86400 # fallback TTL when a template omits it
maxEnvironmentsPerUser: 5
auth:
jwksURL: "https://your-oidc-provider.com/keys"
jwtIssuer: "https://your-oidc-provider.com"
jwtUsernameClaim: preferred_username
oidcClientID: "dploy"
oidcClientSecret: "your-client-secret"
oidcRedirectURL: "https://dploy.your-domain.com/auth/callback"
ingress:
enabled: true
className: nginx
host: dploy.your-domain.com
tls:
- secretName: dploy-tls
hosts:
- dploy.your-domain.com
operator:
enabled: true
# Publish/override the operator image as needed
image:
repository: ghcr.io/aydev-fr/dploy-operator
tag: main
Terminal window
helm install dploy ./charts/dploy \
--namespace dploy-system --create-namespace \
-f values.yaml
Terminal window
helm upgrade dploy ./charts/dploy --namespace dploy-system -f values.yaml

Two images are built and pushed by CI to GHCR:

ComponentImage
APIghcr.io/aydev-fr/dploy
Operatorghcr.io/aydev-fr/dploy-operator

Build them locally with:

Terminal window
make docker-build # API image -> dploy-api:local
make docker-build-operator # operator image -> dploy-operator:local
ParameterDescriptionDefault
config.namespaceNamespace holding the CRs (empty = release namespace)""
config.maxEnvironmentsPerUserPer-user quota (fallback)5
config.defaultTTLDefault initial TTL in seconds86400
config.extendTTLDefault extension granted per /extend7200
auth.jwksURLJWKS endpoint for JWT validation— (required)
auth.jwtIssuerExpected JWT issuer— (required)
auth.jwtAudienceExpected JWT audiencedploy
auth.jwtUsernameClaimClaim used as the usernamepreferred_username
auth.oidcClientID / oidcClientSecretOIDC client credentialsdploy / —
auth.oidcRedirectURLOIDC callback URL
ingress.enabled / className / host / tlsIngress for the API/UIfalse / nginx / …
ParameterDescriptionDefault
operator.enabledDeploy the operatortrue
operator.replicaCountReplicas1
operator.leaderElectionEnable leader election (needed for >1 replica)false
operator.image.repository / tagOperator imageghcr.io/aydev-fr/dploy-operator / main
Terminal window
kubectl get pods -n dploy-system
kubectl get crds | grep dploy.dev
kubectl get dploytemplates,dployinstances -A