CI/CD & Docker
GitHub Actions
Expose PR environments and post the public URL as a comment. Automate testing against real webhook endpoints.
NGRIS_AUTHTOKEN as a GitHub secret. Avoid hardcoding tokens in workflows.
name: Preview with Ngris
on: [pull_request]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build && npm start &
- name: Install Ngris
run: curl -fsSL https://ngris.com/install.sh | sh
- name: Launch tunnel
run: |
ngris http 3000 \
--url pr-${ github.event.number }.preview.ngris.com \
--json &> tunnel.json
- name: Post URL to PR
env:
GITHUB_TOKEN: ${ secrets.GITHUB_TOKEN }
run: |
URL=$(jq -r '.public_url' tunnel.json)
gh pr comment ${ github.event.pull_request.url } --body "🚀 Preview: $URL"
- name: Cleanup on failure
if: failure()
run: |
TUNNEL_ID=$(jq -r '.tunnel_id' tunnel.json)
ngris stop $TUNNEL_ID || true
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" \
--from-literal=NGRIS_SERVER="tunnel.ngris.io:443"
# 2. Helm install
helm install ngris-operator oci://ghcr.io/ngris/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
# Add the Ngris Helm repository
helm repo add ngris https://charts.ngris.com
helm repo update
# Install the agent with custom values
helm upgrade --install ngris-agent ngris/agent \
--namespace ngris \
--create-namespace \
--set agent.authToken='your-auth-token' \
--set agent.tunnels[0].type=http \
--set agent.tunnels[0].addr=3000 \
--set agent.tunnels[0].subdomain=myapp \
--set agent.region=us-east \
--set agent.logLevel=info
# 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