CI/CD & Docker
GitHub Actions — preview on every PR
The official ngris-edge/deploy-action builds your app and deploys a preview to the Ngris edge on every pull request, then comments the live URL back on the PR. Every push updates it — and unlike a plain static host, each preview is served behind the same per-endpoint policy edge (WAF, OAuth, rate‑limits) as your production endpoints.
ngk_), add it as a repository secret named NGRIS_API_KEY, then drop in the workflow. The application is created automatically on the first run.
Inputs
api-key— yourngk_key, from theNGRIS_API_KEYsecret. Required.dir— the build-output directory to serve: Vite/Astro →dist, CRA/SvelteKit-static →build, Next.js static export →out, Nuxt generate →.output/public, Hugo →public.build-command— optional, e.g.npm ci && npm run build. Omit if a previous step builds.app— the app to deploy to (defaults to the repo name);comment— post the URL on the PR (defaulttrue).
Outputs preview-url and deploy-uuid. This deploys static / framework build output; for a managed backend, connect the repo in the dashboard and enable branch auto-preview instead. Full reference: the action README.
name: Preview
on: pull_request
permissions:
contents: read
pull-requests: write # lets the action comment the URL
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- uses: ngris-edge/deploy-action@v1
with:
api-key: ${{ secrets.NGRIS_API_KEY }}
build-command: npm ci && npm run build
dir: dist
Need a live running app in CI instead (end-to-end tests against real webhooks)? Run a tunnel with ngris http during the job and comment its URL — see the Tunnels docs.
Kubernetes Operator
The recommended way to run Ngris on Kubernetes. Declare endpoints, domains, traffic policies, mTLS, and auth as native CRDs; the operator reconciles them against the Ngris API and renders the agent Pods for you. GitOps-friendly, ArgoCD-ready, no CLI on the cluster.
What you get
- 9 CRDs —
Tunnel,TunnelMatrix,NgrisDomain,NgrisEndpoint,NgrisCertificate,NgrisTrafficPolicy,NgrisClientCA,NgrisMTLSAttachment,NgrisAuthPolicy. - Drop-in compatibility with existing
Ingress(setingressClassName: ngris) and Gateway APIHTTPRoute. - Cluster-scoped lifecycle — owner references tear down the agent Pod and Secret on
kubectl delete tunnel. The upstream endpoint is kept by default (it may also be managed from the dashboard / Terraform / another cluster); opt into cascade withannotations.ngris.io/delete-endpoint-on-delete: "true"for ephemeral / per-PR previews. - Status conditions (
Ready,EndpointSynced,AgentDeployed) for ArgoCD / Flux sync waits. - Prometheus metrics built in (
ngris_reconcile_total,ngris_api_requests_total).
Install
Create the operator namespace and a Secret with your API token, then Helm install.
Expose a Service
Three lines of YAML to expose any in-cluster Service via Ngris. Apply, then watch kubectl get tunnel — when READY=True, status.publicURL holds the live URL.
Multi-environment with one CR
TunnelMatrix fans one template into N child Tunnels — one per environment, each with overrides.
Composable security — TrafficPolicy, mTLS, Auth
Each policy attaches to a Tunnel via endpointRef — independent lifecycles, multiple policies per Tunnel, no edits to the Tunnel CR itself. Phase-grouped rules mirror the request lifecycle.
Migrating from existing Ingress
Already have Ingress manifests? Set spec.ingressClassName: ngris and the operator translates each (host × path → Service) into a Tunnel CR. Lossy (no rate-limit / mTLS / auth via Ingress spec — switch to Tunnel + policies for those), but a one-line migration accelerator.
Gateway API
Modern clusters: set controllerName: ngris.io/gateway-controller on a GatewayClass, then write standard HTTPRoute YAML. RequestHeaderModifier / ResponseHeaderModifier filters auto-generate a sibling NgrisTrafficPolicy.
# 1. Operator namespace + token Secret
kubectl create namespace ngris-operator-system
kubectl -n ngris-operator-system create secret generic ngris-operator-config \
--from-literal=NGRIS_API="https://api.ngris.com" \
--from-literal=NGRIS_TOKEN="$NGRIS_TOKEN"
# 2. Helm install
helm install ngris-operator oci://ghcr.io/ngris-edge/charts/ngris-operator \
--namespace ngris-operator-system \
--version 0.1.0
kubectl -n ngris-operator-system rollout status deploy/ngris-operator
apiVersion: ngris.io/v1alpha1
kind: Tunnel
metadata:
name: hello-api
spec:
serviceRef: { name: hello-api, port: 8080 }
host: hello.example.com # longest-suffix-match → parent NgrisDomain + subdomain
protocol: http
region: eu-north-1
apiVersion: ngris.io/v1alpha1
kind: TunnelMatrix
metadata: { name: hello-api }
spec:
template:
serviceRef: { name: hello-api, port: 8080 }
protocol: http
region: eu-north-1
environments:
- { name: dev, host: hello-dev.example.com }
- { name: staging, host: hello-staging.example.com }
- { name: prod, host: hello.example.com, region: us-east-1 }
apiVersion: ngris.io/v1alpha1
kind: NgrisTrafficPolicy
metadata: { name: anon-rate-limit }
spec:
endpointRef: { name: hello-api }
on_http_request:
- type: rate-limit
config: { requests_per_minute: 60, burst: 100, key: ip }
- type: jwt-validation
config: { jwks_url: "https://issuer/.well-known/jwks.json" }
on_http_response:
- type: add-headers
config: { headers: { x-served-by: ngris } }
---
apiVersion: ngris.io/v1alpha1
kind: NgrisMTLSAttachment
metadata: { name: hello-api-mtls }
spec:
endpointRef: { name: hello-api }
mode: required
caRefs: [{ name: internal-corp-ca }]
Docker & Kubernetes (manual)
Run Ngris in containers when you don't want the operator. Supports Docker Compose and Helm.
Docker Compose (Local Development)
Deploy both your app and tunnel in a single compose file.
.env file to manage NGRIS_AUTHTOKEN locally. Never commit it to version control.
Kubernetes with Helm (Production)
Deploy Ngris as a sidecar or standalone pod in Kubernetes clusters.
Option 1: Helm Chart (Recommended)
Add the Ngris Helm repository and install the agent with custom values.
Option 2: Manual Deployment YAML
Render the Deployment + ConfigMap + Secret yourself if you want full control over the manifests.
Option 3: Sidecar Deployment
Deploy Ngris as a sidecar container within your application pod for seamless integration and lifecycle management.
Sidecar Configuration Explained
- Startup Sequence: The init container waits for the database, then the main application starts. The Ngris sidecar waits for the application's health endpoint before establishing the tunnel.
- Network Sharing: Both containers share the same network namespace (default behavior), allowing the sidecar to access the main container via localhost.
- Dynamic Subdomains: Environment variables allow dynamic subdomain naming based on pod name, shard, or other identifiers.
- Resource Management: The sidecar has defined resource limits to prevent it from consuming excessive cluster resources.
- Health Monitoring: Startup probe ensures the Ngris process is running, complementing the main application's readiness/liveness probes.
Benefits of Sidecar Pattern
- Tight Lifecycle Coupling: The tunnel is automatically created when the pod starts and destroyed when the pod terminates.
- Simplified Networking: No need for service discovery or DNS lookups - the sidecar connects directly to localhost.
- Enhanced Security: Authentication tokens are scoped to the specific pod/deployment rather than cluster-wide.
- Better Observability: Tunnel logs are co-located with application logs, making debugging easier.
- Granular Control: Different deployments can have different tunnel configurations, regions, and access controls.
Advanced Sidecar Configuration
For complex scenarios, use a ConfigMap to manage Ngris configuration.
- Always store authentication tokens in Kubernetes Secrets, never in plain text.
- Consider using tools like HashiCorp Vault or AWS Secrets Manager for dynamic secret injection.
- Implement network policies to restrict which pods can communicate with the Ngris sidecar.
- Regularly rotate authentication tokens and monitor for unauthorized endpoint creation.
kubectl exec -it <pod-name> -c ngris -- sh to access its shell. You can then run ngris list or check logs directly.
# docker-compose.yaml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
tunnel:
image: ghcr.io/ngris-edge/ngris:latest
command: >
sh -c "
while ! nc -z app 3000; do
echo 'Waiting for app to start...';
sleep 2;
done;
ngris http app:3000 --url myapp.ngris.com --log-level debug
"
environment:
- NGRIS_AUTHTOKEN=${NGRIS_AUTHTOKEN}
depends_on:
- app
networks:
- app-network
networks:
app-network:
driver: bridge
# Install the ngris operator from GHCR — OCI, no `helm repo add` needed
helm install ngris-operator oci://ghcr.io/ngris-edge/charts/ngris-operator \
--namespace ngris-operator-system --create-namespace \
--version 0.1.0
# Configure the token Secret first (see the Operator setup above),
# then declare your tunnels/endpoints as CRDs: kubectl apply -f tunnel.yaml
# k8s-ngris.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngris-agent
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: ngris-agent
template:
metadata:
labels:
app: ngris-agent
spec:
containers:
- name: ngris
image: ghcr.io/ngris-edge/ngris:latest
command: ["ngris", "start"]
env:
- name: NGRIS_AUTHTOKEN
valueFrom:
secretKeyRef:
name: ngris-secrets
key: auth-token
- name: NGRIS_REGION
value: "us-east"
volumeMounts:
- name: config
mountPath: /etc/ngris
readOnly: true
volumes:
- name: config
configMap:
name: ngris-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ngris-config
data:
ngris.yaml: |
agent:
region: us-east
tunnels:
- name: web-service
type: http
addr: web-service:8080
url: prod-api
---
apiVersion: v1
kind: Secret
metadata:
name: ngris-secrets
type: Opaque
data:
auth-token: BASE64_ENCODED_TOKEN_HERE
# deployment-with-sidecar.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z myapp-db 5432; do echo "Waiting for database..."; sleep 2; done;']
containers:
- name: myapp
image: myorg/myapp:latest
ports:
- containerPort: 3000
name: http
env:
- name: DATABASE_URL
value: "postgresql://user:pass@myapp-db:5432/mydb"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
- name: ngris
image: ghcr.io/ngris-edge/ngris:latest
command:
- sh
- -c
- |
while ! wget -qO- http://localhost:3000/health >/dev/null 2>&1; do
echo "Waiting for myapp to start..."
sleep 2
done
ngris http 3000 \
--url $(SUBDOMAIN).ngris.com \
--name $(POD_NAME) \
--log-level info
env:
- name: SUBDOMAIN
value: myapp-${SHARD}
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: SHARD
value: "prod"
- name: NGRIS_AUTHTOKEN
valueFrom:
secretKeyRef:
name: ngris-secrets
key: auth-token
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
# advanced-sidecar.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ngris-config
data:
ngris.yaml: |
agent:
region: us-east
log_level: debug
stats_server: true
http_port: 9000
tunnels:
- name: primary
type: http
addr: 3000
url: ${NGRIS_SUBDOMAIN}
access:
password: "${NGRIS_PASSWORD}"
inspector:
capture_bodies: true
- name: admin
type: http
addr: 3001
url: admin-${NGRIS_SUBDOMAIN}
require_sso: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-advanced
spec:
template:
spec:
containers:
- name: myapp
# ... application container config ...
- name: ngris
image: ghcr.io/ngris-edge/ngris:latest
command: ["ngris", "start", "--config", "/etc/ngris/ngris.yaml"]
env:
- name: NGRIS_SUBDOMAIN
value: "myapp-prod"
- name: NGRIS_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: tunnel-password
volumeMounts:
- name: config
mountPath: /etc/ngris
readOnly: true
volumes:
- name: config
configMap:
name: ngris-config