CI/CD & Docker

GitHub Actions

Expose PR environments and post the public URL as a comment. Automate testing against real webhook endpoints.

Security Note: Always store NGRIS_AUTHTOKEN as a GitHub secret. Avoid hardcoding tokens in workflows.
.github/workflows/preview.yaml
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 CRDsTunnel, TunnelMatrix, NgrisDomain, NgrisEndpoint, NgrisCertificate, NgrisTrafficPolicy, NgrisClientCA, NgrisMTLSAttachment, NgrisAuthPolicy.
  • Drop-in compatibility with existing Ingress (set ingressClassName: ngris) and Gateway API HTTPRoute.
  • 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 with annotations.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.

Source & full reference: github.com/ngris/ngris-operator — CRD reference, Helm values, status condition vocabulary, and the kind-based E2E test suite.
Install
# 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
Tunnel CR
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
TunnelMatrix
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 }
TrafficPolicy + mTLS
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.

Tip: Use .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.

Security Considerations for Sidecars:
  • 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.
Debugging Tip: To inspect the Ngris sidecar, use 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
# 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
Helm chart
# 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
Manual deployment 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
Sidecar deployment
# 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 with ConfigMap
# 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
Iris