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.

Setup (once): create an API key under Settings → API keys (it starts with 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 — your ngk_ key, from the NGRIS_API_KEY secret. 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 (default true).

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.

.github/workflows/preview.yml
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 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"

# 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
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
# 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
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