-
Notifications
You must be signed in to change notification settings - Fork 0
deployment
Conduit is a single static binary with no runtime dependencies. It runs on bare metal, VMs, containers, and Kubernetes.
Not sure what config to write first? ← Configuration Recipes
Two image variants are published to the GitHub Container Registry on every release:
| Variant | Tags | Includes |
|---|---|---|
| Standard |
:latest, :1.1.2, :1.1
|
standard feature bundle: jwt, consumers, forward-auth, cache, acme — covers the typical self-hosted reverse-proxy / API gateway |
| Full |
:latest-full, :1.1.2-full, :1.1-full
|
All 14 optional features: jwt, consumers, forward-auth, rhai, wasm, tcp, upload, redis, cache, disk-cache, acme, fault-injection, otlp, kubernetes
|
# Standard (jwt, consumers, forward-auth, cache, acme)
docker pull ghcr.io/lopatnov/conduit:latest
# Full — all optional features enabled
docker pull ghcr.io/lopatnov/conduit:latest-fullStandard vs Full: the standard image covers the majority of production use cases — TLS, proxying, routing, rate limiting, basic/API-key/JWT auth, the consumer model, ForwardAuth, response caching, auto-TLS (ACME), metrics, hot-reload. Use the full image (or build from source with
--features) when you need scripting middleware (Rhai/WASM), file uploads, Redis-backed rate limiting/caching, OTLP tracing, TCP proxy mode, or Kubernetes CRD config.
Both images are multi-stage musl builds packaged into FROM scratch.
They run as UID 65534 (nobody) with no shell or OS userland.
Healthchecks in FROM scratch images: because the image has no shell or system utilities (
wget,curl), DockerCMD-SHELLhealthchecks do not work. Useconduit statusvia the Admin API (requiresglobal.adminin config), or rely on KuberneteshttpGetprobes which are executed externally by kubelet — not inside the container.
docker run -p 8080:8080 \
-v /path/to/conduit.yaml:/etc/conduit/conduit.yaml:ro \
ghcr.io/lopatnov/conduit:latest -c /etc/conduit/conduit.yamlFor TLS, also mount cert files:
docker run -p 443:443 -p 80:80 \
-v /path/to/conduit.yaml:/etc/conduit/conduit.yaml:ro \
-v /etc/tls:/etc/tls:ro \
ghcr.io/lopatnov/conduit:latest -c /etc/conduit/conduit.yamlservices:
conduit:
image: ghcr.io/lopatnov/conduit:latest
ports:
- "443:443"
- "80:80"
volumes:
- ./conduit.yaml:/etc/conduit/conduit.yaml:ro
- ./dist:/dist:ro
- ./certs:/var/cache/conduit/certs # ACME cert storage
environment:
METRICS_TOKEN: "${METRICS_TOKEN}"
ADMIN_TOKEN: "${ADMIN_TOKEN}"
restart: unless-stopped
# NOTE: FROM scratch has no shell/wget — use conduit status if Admin API
# is configured, or rely on external healthchecks (load balancer, K8s probe)
healthcheck:
test: ["CMD", "/conduit", "status", "--admin", "127.0.0.1:2019"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
api:
image: my-api:latest
expose: ["4000"]The
healthcheckabove requiresglobal.admin.bind: "127.0.0.1:2019"inconduit.yaml. Remove the healthcheck block if the Admin API is not configured.
Environment variables referenced as $VAR in the config file are substituted
at startup. Pass secrets via environment: — never bake them into the image.
git clone https://github.com/lopatnov/conduit
cd conduit
# Minimal image (default = [])
docker build -f contrib/Dockerfile -t conduit:local .
# Standard image (matches the published default tag)
docker build -f contrib/Dockerfile \
--build-arg FEATURES=standard \
-t conduit:local-standard .
# Full-features image
docker build -f contrib/Dockerfile \
--build-arg FEATURES=full \
-t conduit:local-full .The contrib/conduit.service unit file is ready for production use. It includes
security hardening (NoNewPrivileges, ProtectSystem=strict) and automatic
restart on failure.
Step 1 — Get the binary.
Download a pre-built binary from GitHub Releases:
# Linux x86-64 (standard: jwt, consumers, forward-auth, cache, acme)
curl -L https://github.com/lopatnov/conduit/releases/latest/download/conduit-x86_64-unknown-linux-gnu.tar.gz \
| tar xz
sudo install -m 755 conduit /usr/local/bin/conduit
# Linux x86-64 (full — adds Rhai/WASM, OTLP, TCP proxy, Redis, uploads, Kubernetes, etc.)
curl -L https://github.com/lopatnov/conduit/releases/latest/download/conduit-x86_64-unknown-linux-gnu-full.tar.gz \
| tar xz
sudo install -m 755 conduit /usr/local/bin/conduitOr build from source — see docs/building.md.
Step 2 — Create user, directories, and install service:
# Create a dedicated system user (no home dir, no shell)
sudo useradd --system --no-create-home --shell /usr/sbin/nologin conduit
# Create directories
sudo mkdir -p /etc/conduit /var/log/conduit /var/cache/conduit
# Install config (edit before starting)
sudo cp conduit.yaml /etc/conduit/conduit.yaml
sudo chown -R conduit:conduit /etc/conduit /var/log/conduit /var/cache/conduit
# Install and enable service
sudo cp contrib/conduit.service /etc/systemd/system/conduit.service
sudo systemctl daemon-reload
sudo systemctl enable --now conduitPut environment variables (tokens, passwords, keys) in /etc/conduit/conduit.env:
# /etc/conduit/conduit.env
METRICS_TOKEN=my-scrape-token
ADMIN_TOKEN=my-admin-token
JWT_SECRET=my-jwt-secret
API_KEY=my-api-keySet permissions so only the conduit user can read it:
sudo chmod 600 /etc/conduit/conduit.env
sudo chown conduit:conduit /etc/conduit/conduit.envReference them in conduit.yaml:
metrics:
token: "$METRICS_TOKEN"
jwtAuth:
secret: "$JWT_SECRET"The unit file loads conduit.env automatically via EnvironmentFile=.
Key parts of contrib/conduit.service:
[Service]
User=conduit
Group=conduit
WorkingDirectory=/etc/conduit
ExecStart=/usr/local/bin/conduit -c /etc/conduit/conduit.yaml
ExecReload=/usr/local/bin/conduit reload # ← enables `systemctl reload`
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
# Hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/log/conduit /var/cache/conduit
EnvironmentFile=-/etc/conduit/conduit.envExecReload wires systemctl reload conduit to a zero-downtime hot-reload via
the Admin API — no connections are dropped.
# Check status
sudo systemctl status conduit
# View logs (live)
sudo journalctl -u conduit -f
# Hot-reload config (zero dropped connections)
sudo systemctl reload conduit
# or directly:
conduit reload --admin 127.0.0.1:2019
# Restart (required when port, TLS cert/key, or workers change)
sudo systemctl restart conduit
# Graceful shutdown
conduit shutdown --admin 127.0.0.1:2019
# Check upstream health
conduit status --upstream --admin 127.0.0.1:2019# /etc/logrotate.d/conduit
/var/log/conduit/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
postrotate
# Reload to switch to the new log file atomically.
# If Admin API is on a non-default port, update the --admin flag.
conduit reload --admin 127.0.0.1:2019 || true
endscript
}Conduit is not a Kubernetes-native ingress controller, but it fits well in several deployment patterns.
Run Conduit alongside your application container. Conduit handles TLS termination, auth, rate limiting, and static file serving; your app handles business logic only.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
initContainers:
# Copy frontend build into shared volume
- name: copy-static
image: myapp-frontend:latest
command: ["cp", "-r", "/build/.", "/dist"]
volumeMounts:
- name: static-files
mountPath: /dist
containers:
- name: api
image: myapp:latest
ports:
- containerPort: 4000
- name: conduit
image: ghcr.io/lopatnov/conduit:latest
args: ["-c", "/etc/conduit/conduit.yaml"]
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: conduit-config
mountPath: /etc/conduit
- name: static-files
mountPath: /dist
readinessProbe:
httpGet: { path: /__health__, port: 8080 }
initialDelaySeconds: 2
livenessProbe:
httpGet: { path: /__health__, port: 8080 }
periodSeconds: 10
env:
- name: METRICS_TOKEN
valueFrom:
secretKeyRef: { name: conduit-secrets, key: metrics-token }
volumes:
- name: conduit-config
configMap:
name: conduit-config
- name: static-files
emptyDir: {}
---
# Expose via a Service
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- name: http
port: 80
targetPort: 8080Deploy one Conduit per node as a lightweight node-local ingress or traffic forwarder.
Security note:
hostNetwork: truegives the pod full access to the node's network namespace. Only use this when necessary (e.g. binding privileged ports < 1024 on a node withoutCAP_NET_BIND_SERVICE). Prefer a standard Deployment + Service + NodePort for most use cases.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: conduit-proxy
spec:
selector:
matchLabels:
app: conduit-proxy
template:
metadata:
labels:
app: conduit-proxy
spec:
hostNetwork: true # ← grants node network access; see security note above
containers:
- name: conduit
image: ghcr.io/lopatnov/conduit:latest
args: ["-c", "/etc/conduit/conduit.yaml"]
ports:
- containerPort: 8080
hostPort: 8080
readinessProbe:
httpGet: { path: /__health__, port: 8080 }
initialDelaySeconds: 2
volumeMounts:
- name: config
mountPath: /etc/conduit
volumes:
- name: config
configMap:
name: conduit-configStore the config as a ConfigMap so it can be updated without rebuilding images.
Reference secrets from Kubernetes Secrets using valueFrom.secretKeyRef.
# Create ConfigMap from local file
kubectl create configmap conduit-config \
--from-file=conduit.yaml=./conduit.yaml
# Update after config change (atomic apply)
kubectl create configmap conduit-config \
--from-file=conduit.yaml=./conduit.yaml \
--dry-run=client -o yaml | kubectl apply -f -Hot-reload after ConfigMap update (requires Admin API in config):
# Port-forward to a pod's Admin API, then reload
kubectl port-forward pod/myapp-xyz 2019:2019 &
conduit reload --admin 127.0.0.1:2019For automatic hot-reload on ConfigMap changes, use the ConduitSite CRD instead — it watches Kubernetes events directly without polling.
Requires
cargo build --features kubernetes— use the:latest-fullDocker image.
Instead of a config file or ConfigMap, configure Conduit with Kubernetes custom
resources. Each ConduitSite object maps to one virtual site. Changes are
applied automatically — no conduit reload needed.
How it works:
- Conduit connects to the cluster (via
KUBECONFIGor in-cluster service account) - Reads all
ConduitSiteresources in the given namespace - Combines them into a running multi-site config
- Watches
Added/Modified/Deletedevents → hot-reloads automatically
Step 1 — Install the CRD definition (once per cluster):
kubectl apply -f contrib/k8s/conduitsite-crd.yamlStep 2 — Create RBAC resources:
# contrib/k8s/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: conduit
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: conduit-crd-reader
namespace: default
rules:
- apiGroups: ["conduit.io"]
resources: ["conduitsites"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: conduit-crd-reader
namespace: default
subjects:
- kind: ServiceAccount
name: conduit
roleRef:
kind: Role
name: conduit-crd-reader
apiGroup: rbac.authorization.k8s.ioFor all-namespaces mode (--kubernetes-namespace '*'), use ClusterRole and
ClusterRoleBinding instead of Role and RoleBinding.
kubectl apply -f contrib/k8s/rbac.yamlStep 3 — Deploy Conduit with the CRD flag:
# conduit-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: conduit
spec:
replicas: 1
selector:
matchLabels: { app: conduit }
template:
metadata:
labels: { app: conduit }
spec:
serviceAccountName: conduit
containers:
- name: conduit
image: ghcr.io/lopatnov/conduit:latest-full
args: ["--kubernetes-namespace", "default"]
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /__health__, port: 8080 }conduit --kubernetes-namespace default
conduit --kubernetes-namespace '*' # all namespaces (requires ClusterRole)Step 4 — Create ConduitSite resources:
# my-app-site.yaml
apiVersion: conduit.io/v1
kind: ConduitSite
metadata:
name: my-app
namespace: default
spec:
port: 8080
proxy: "http://my-svc:4000"
healthCheck: true
rateLimit:
windowSecs: 60
limit: 500# my-api-site.yaml — TLS + JWT auth
apiVersion: conduit.io/v1
kind: ConduitSite
metadata:
name: my-api
namespace: default
spec:
port: 443
host: api.example.com
tls:
acme:
email: admin@example.com
storage: /var/cache/conduit/certs # mount a PVC at this path
jwtAuth:
jwksUrl: "https://auth.example.com/.well-known/jwks.json"
proxy:
/v1: "http://api-svc:4000"
healthCheck: truekubectl apply -f my-app-site.yaml
kubectl apply -f my-api-site.yaml
# Conduit detects the new resources and hot-reloads within secondsThe spec fields mirror the Conduit config schema exactly — every field valid
in conduit.yaml works in spec.
Update a site (zero-downtime):
kubectl patch conduitsite my-app \
-p '{"spec":{"rateLimit":{"limit":1000}}}' --type=merge
# Hot-reloads within secondsDelete a site:
kubectl delete conduitsite my-app| Feature | nginx-ingress | Conduit |
|---|---|---|
| TLS termination | ✅ | ✅ |
| Auto-TLS (ACME) | via cert-manager | ✅ built-in |
| Path-based routing | ✅ | ✅ |
| Load balancing | ✅ | ✅ 8 strategies |
| Rate limiting | via annotations | ✅ native |
| JWT / API key auth | via plugin | ✅ native |
| Prometheus metrics | via exporter | ✅ native |
| Config format | YAML annotations | JSON / YAML file or CRDs |
| Kubernetes CRDs | ✅ | ✅ --features kubernetes
|
| Binary size | ~50 MB | ~18 MB |
Conduit is a good fit when you want a simple, self-contained proxy without the overhead of a full Ingress controller. For large clusters with many teams and services, a dedicated controller (nginx, Traefik, Envoy Gateway) scales better.
Conduit supports in-place binary replacement with no dropped connections:
# 1. Download the new binary alongside the old one
curl -L https://github.com/lopatnov/conduit/releases/latest/download/conduit-x86_64-unknown-linux-gnu.tar.gz \
| tar xz
sudo install -m 755 conduit /usr/local/bin/conduit.new
# 2. Atomically replace the binary (rename is atomic on Linux)
sudo mv /usr/local/bin/conduit.new /usr/local/bin/conduit
# 3. Restart to load the new binary
# Use reload first to pick up any config changes,
# then restart only if the port/TLS/workers changed.
sudo systemctl restart conduitConduit's graceful shutdown (
shutdownTimeoutSecs) ensures in-flight requests complete before the process exits. Setglobal.shutdownTimeoutSecs: 30in your config to allow up to 30 s.
# Update the image tag — Kubernetes handles the rolling restart
kubectl set image deployment/myapp conduit=ghcr.io/lopatnov/conduit:1.1.2
kubectl rollout status deployment/myapp
# Roll back if needed
kubectl rollout undo deployment/myappBefore first deploy:
# 1. Validate config — exits 0 if OK, exits 1 with field-level errors
conduit validate -c conduit.yaml
# 2. Probe all upstreams — exits 1 if any are unreachable
conduit probe -c conduit.yamlConfig checklist:
-
port: 443withtls+httpRedirectPort: 80for HTTPS -
securityHeaders: true(or custom values) -
logging: jsonfor structured log ingestion -
metrics.tokenset to prevent unauthenticated scraping -
global.admin.tokenset if Admin API is exposed - All secrets in environment variables (
"$VAR"), not hardcoded -
healthCheck: truefor readiness/liveness probes -
maskErrors: trueto hide upstream stack traces -
rateLimitconfigured on all public endpoints -
limits.maxInflightRequestsset to cap concurrent load -
limits.maxBodyBytesset to reject oversized request bodies -
retry.budgetPercentset to prevent retry storms -
outlierDetectionenabled for passive health tracking - Firewall: only ports 80/443 exposed publicly; port 2019 loopback-only
After deploy:
# Verify server is healthy
conduit status --admin 127.0.0.1:2019
# Verify all upstreams are reachable
conduit status --upstream --admin 127.0.0.1:2019
# Check Prometheus metrics are reachable
curl -H "Authorization: Bearer $METRICS_TOKEN" \
https://your-host/__metrics__ | head -5