HTTP Router Guide

    Skipper HTTP Router

    Deploy Skipper, the powerful HTTP router and reverse proxy for service composition, on RamNode VPS. Advanced routing, custom filters, and native Kubernetes Ingress support.

    Ubuntu/Debian
    K8s Ingress
    ⏱️ 20-30 minutes

    Prerequisites & VPS Selection

    Standalone

    • • 1GB RAM
    • • 1 vCPU
    • • Simple routing

    Recommended

    • • 2GB RAM
    • • 2 vCPU
    • • K8s Ingress

    Production

    • • 4GB+ RAM
    • • 4 vCPU
    • • High traffic

    💡 Tip: For Kubernetes deployments, see our Kubernetes deployment guide first to set up your cluster.

    2

    Install Skipper

    Install Skipper using pre-built binaries or Docker:

    Option 1: Binary Installation

    Download and install Skipper
    # Download latest release
    SKIPPER_VERSION=$(curl -s https://api.github.com/repos/zalando/skipper/releases/latest | grep tag_name | cut -d '"' -f 4)
    wget https://github.com/zalando/skipper/releases/download/${SKIPPER_VERSION}/skipper-linux-amd64
    
    # Make executable and move to PATH
    chmod +x skipper-linux-amd64
    sudo mv skipper-linux-amd64 /usr/local/bin/skipper
    
    # Verify installation
    skipper -version

    Option 2: Docker Installation

    Run Skipper with Docker
    # Pull the official image
    docker pull registry.opensource.zalan.do/teapot/skipper:latest
    
    # Run Skipper container
    docker run -d --name skipper \
      -p 9090:9090 \
      -p 9911:9911 \
      -v /etc/skipper:/etc/skipper \
      registry.opensource.zalan.do/teapot/skipper:latest \
      skipper -routes-file /etc/skipper/routes.eskip

    Create Systemd Service

    /etc/systemd/system/skipper.service
    [Unit]
    Description=Skipper HTTP Router
    After=network.target
    
    [Service]
    Type=simple
    User=skipper
    ExecStart=/usr/local/bin/skipper \
        -routes-file /etc/skipper/routes.eskip \
        -address :9090 \
        -support-listener :9911 \
        -access-log-disabled=false
    Restart=always
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target
    Enable and start service
    # Create skipper user
    sudo useradd -r -s /bin/false skipper
    
    # Create config directory
    sudo mkdir -p /etc/skipper
    
    # Enable and start
    sudo systemctl daemon-reload
    sudo systemctl enable skipper
    sudo systemctl start skipper
    3

    Basic Configuration

    Skipper uses eskip format for route definitions. Create your routes file:

    /etc/skipper/routes.eskip
    // Simple proxy route
    backend: Path("/api/*") -> "http://localhost:8080";
    
    // Static response
    health: Path("/health") -> status(200) -> inlineContent("OK") -> <shunt>;
    
    // Host-based routing
    app: Host("^app\.example\.comquot;) -> "http://localhost:3000";
    api: Host("^api\.example\.comquot;) -> "http://localhost:8080";
    
    // Path prefix routing
    docs: PathSubtree("/docs") -> "http://localhost:4000";
    
    // Default catch-all
    catchAll: * -> status(404) -> inlineContent("Not Found") -> <shunt>;

    Route Syntax

    Predicates

    Match conditions like Path(), Host(), Method(), Header()

    Filters

    Transform requests/responses like setPath(), setHeader()

    Backends

    Destination URLs or special backends like <shunt>, <loopback>

    Route ID

    Named identifier for the route, used in metrics and logging

    Test configuration
    # Validate routes file
    skipper -routes-file /etc/skipper/routes.eskip -print-routes
    
    # Reload routes (if running)
    sudo systemctl reload skipper
    4

    Route Definitions

    Advanced routing patterns for different use cases:

    Path and method routing
    // REST API routing
    getUsers: Method("GET") && Path("/users") -> "http://users-service:8080";
    createUser: Method("POST") && Path("/users") -> "http://users-service:8080";
    getUser: Method("GET") && PathRegexp("/users/[0-9]+") -> "http://users-service:8080";
    
    // Query parameter matching
    search: Path("/search") && QueryParam("q") -> "http://search-service:8080";
    
    // Header-based routing
    mobileApp: Header("X-App-Type", "mobile") -> "http://mobile-api:8080";
    webApp: Header("X-App-Type", "web") -> "http://web-api:8080";
    Traffic splitting (canary/blue-green)
    // Canary deployment - 10% to new version
    canary: Path("/api/*")
        -> Traffic(0.1)
        -> "http://api-v2:8080";
    
    stable: Path("/api/*")
        -> "http://api-v1:8080";
    
    // A/B testing based on cookie
    abTestA: Path("/feature/*") && Cookie("ab_test", "variant_a")
        -> "http://feature-v1:8080";
    
    abTestB: Path("/feature/*") && Cookie("ab_test", "variant_b")
        -> "http://feature-v2:8080";
    Request/response modification
    // Modify path before proxying
    rewritePath: PathSubtree("/legacy/api")
        -> modPath("^/legacy", "")
        -> "http://new-api:8080";
    
    // Add headers
    addHeaders: Path("/api/*")
        -> setRequestHeader("X-Forwarded-Proto", "https")
        -> setRequestHeader("X-Request-ID", "${request_id}")
        -> setResponseHeader("X-Served-By", "skipper")
        -> "http://backend:8080";
    
    // CORS handling
    cors: Path("/api/*")
        -> corsOrigin("https://app.example.com")
        -> "http://backend:8080";
    5

    Built-in Filters

    Skipper includes powerful built-in filters for common operations:

    Rate limiting
    // Client rate limiting (10 req/sec per IP)
    rateLimited: Path("/api/*")
        -> clientRatelimit(10, "1s")
        -> "http://backend:8080";
    
    // Cluster rate limiting (shared across instances)
    clusterRateLimited: Path("/api/*")
        -> clusterClientRatelimit("api-limit", 100, "1s")
        -> "http://backend:8080";
    
    // Backend rate limiting
    backendLimited: Path("/slow-api/*")
        -> backendRatelimit("slow-backend", 5, "1s")
        -> "http://slow-backend:8080";
    Circuit breaker
    // Circuit breaker with 5 consecutive failures
    circuitBreaker: Path("/api/*")
        -> consecutiveBreaker(5)
        -> "http://backend:8080";
    
    // Rate-based circuit breaker (50% failure rate)
    rateBreaker: Path("/api/*")
        -> rateBreaker(10, 0.5, 30)
        -> "http://backend:8080";
    Compression and caching
    // Enable compression
    compressed: Path("/api/*")
        -> compress()
        -> "http://backend:8080";
    
    // Response caching
    cached: Path("/static/*")
        -> fifo(100, 50, "10s")
        -> "http://static-backend:8080";
    Request/response body handling
    // Log request body
    logBody: Path("/api/*")
        -> teeBody("request")
        -> "http://backend:8080";
    
    // Modify response body (JSON)
    modifyResponse: Path("/api/user")
        -> modifyResponseBodyScript(`
            doc.timestamp = new Date().toISOString();
            return doc;
        `)
        -> "http://backend:8080";
    6

    Kubernetes Integration

    Deploy Skipper as a Kubernetes Ingress Controller. See our Kubernetes guide for cluster setup.

    skipper-ingress-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: skipper-ingress
      namespace: kube-system
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: skipper-ingress
      template:
        metadata:
          labels:
            app: skipper-ingress
        spec:
          serviceAccountName: skipper-ingress
          containers:
          - name: skipper-ingress
            image: registry.opensource.zalan.do/teapot/skipper:latest
            args:
              - skipper
              - -kubernetes
              - -kubernetes-in-cluster
              - -kubernetes-path-mode=path-prefix
              - -address=:9090
              - -wait-first-route-load
              - -proxy-preserve-host
              - -serve-host-metrics
              - -enable-ratelimits
              - -experimental-upgrade
              - -metrics-exp-decay-sample
              - -lb-healthcheck-interval=3s
              - -metrics-flavour=prometheus
              - -enable-connection-metrics
            ports:
            - containerPort: 9090
            - containerPort: 9911
            resources:
              requests:
                cpu: 100m
                memory: 128Mi
              limits:
                cpu: 1000m
                memory: 512Mi
            readinessProbe:
              httpGet:
                path: /kube-system/healthz
                port: 9911
              initialDelaySeconds: 5
              timeoutSeconds: 5
    skipper-service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: skipper-ingress
      namespace: kube-system
    spec:
      type: LoadBalancer
      selector:
        app: skipper-ingress
      ports:
      - name: http
        port: 80
        targetPort: 9090
      - name: https
        port: 443
        targetPort: 9443
    RBAC configuration
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: skipper-ingress
      namespace: kube-system
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: skipper-ingress
    rules:
    - apiGroups: [""]
      resources: ["services", "endpoints", "secrets"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["networking.k8s.io"]
      resources: ["ingresses"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["networking.k8s.io"]
      resources: ["ingresses/status"]
      verbs: ["patch", "update"]
    - apiGroups: ["zalando.org"]
      resources: ["routegroups"]
      verbs: ["get", "list", "watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: skipper-ingress
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: skipper-ingress
    subjects:
    - kind: ServiceAccount
      name: skipper-ingress
      namespace: kube-system

    Ingress with Skipper Annotations

    ingress-example.yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: my-app-ingress
      annotations:
        # Rate limiting
        zalando.org/skipper-filter: ratelimit(20, "1m")
        # Path rewriting
        zalando.org/skipper-predicate: PathSubtree("/api")
        # Backend timeout
        zalando.org/backend-timeout: "30s"
    spec:
      ingressClassName: skipper
      rules:
      - host: app.example.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

    RouteGroup CRD (Advanced)

    routegroup-example.yaml
    apiVersion: zalando.org/v1
    kind: RouteGroup
    metadata:
      name: my-api
    spec:
      hosts:
      - api.example.com
      backends:
      - name: api-v1
        type: service
        serviceName: api-v1
        servicePort: 80
      - name: api-v2
        type: service
        serviceName: api-v2
        servicePort: 80
      routes:
      - pathSubtree: /v1
        backends:
        - backendName: api-v1
      - pathSubtree: /v2
        backends:
        - backendName: api-v2
      # Canary with traffic split
      - pathSubtree: /api
        filters:
        - ratelimit(100, "1m")
        backends:
        - backendName: api-v1
          weight: 90
        - backendName: api-v2
          weight: 10
    Deploy to Kubernetes
    # Apply RBAC
    kubectl apply -f skipper-rbac.yaml
    
    # Deploy Skipper
    kubectl apply -f skipper-ingress-deployment.yaml
    kubectl apply -f skipper-service.yaml
    
    # Verify deployment
    kubectl get pods -n kube-system -l app=skipper-ingress
    kubectl get svc -n kube-system skipper-ingress
    7

    Custom Filter Plugins

    Create custom filters in Go to extend Skipper's functionality:

    custom_filter.go
    package main
    
    import (
        "github.com/zalando/skipper/filters"
        "net/http"
    )
    
    // Filter specification
    type customAuthSpec struct{}
    
    func (s *customAuthSpec) Name() string {
        return "customAuth"
    }
    
    func (s *customAuthSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
        if len(args) != 1 {
            return nil, filters.ErrInvalidFilterParameters
        }
        
        secret, ok := args[0].(string)
        if !ok {
            return nil, filters.ErrInvalidFilterParameters
        }
        
        return &customAuthFilter{secret: secret}, nil
    }
    
    // Filter implementation
    type customAuthFilter struct {
        secret string
    }
    
    func (f *customAuthFilter) Request(ctx filters.FilterContext) {
        req := ctx.Request()
        
        // Check API key header
        apiKey := req.Header.Get("X-API-Key")
        if apiKey != f.secret {
            ctx.Serve(&http.Response{
                StatusCode: 401,
                Header:     http.Header{"Content-Type": []string{"application/json"}},
            })
            return
        }
        
        // Add authenticated header
        req.Header.Set("X-Authenticated", "true")
    }
    
    func (f *customAuthFilter) Response(ctx filters.FilterContext) {
        // Optional: modify response
    }
    main.go - Register custom filters
    package main
    
    import (
        "github.com/zalando/skipper"
        "github.com/zalando/skipper/filters"
    )
    
    func main() {
        // Register custom filter
        filters.Register(&customAuthSpec{})
        
        // Additional custom filters
        filters.Register(&rateLimitByUserSpec{})
        filters.Register(&requestLoggerSpec{})
        
        // Start Skipper with custom filters
        skipper.Run(skipper.Options{
            Address:          ":9090",
            RoutesFile:       "/etc/skipper/routes.eskip",
            CustomFilters:    []filters.Spec{
                &customAuthSpec{},
                &rateLimitByUserSpec{},
            },
        })
    }
    Build custom Skipper
    # Initialize Go module
    go mod init custom-skipper
    go mod tidy
    
    # Build
    go build -o skipper-custom .
    
    # Run with custom filters
    ./skipper-custom -routes-file routes.eskip

    Example: User-based Rate Limiting Filter

    ratelimit_by_user.go
    package main
    
    import (
        "github.com/zalando/skipper/filters"
        "sync"
        "time"
    )
    
    type rateLimitByUserSpec struct{}
    
    func (s *rateLimitByUserSpec) Name() string {
        return "rateLimitByUser"
    }
    
    func (s *rateLimitByUserSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
        if len(args) != 2 {
            return nil, filters.ErrInvalidFilterParameters
        }
        
        limit, _ := args[0].(int)
        window, _ := args[1].(string)
        duration, _ := time.ParseDuration(window)
        
        return &rateLimitByUserFilter{
            limit:    limit,
            window:   duration,
            counters: make(map[string]*counter),
        }, nil
    }
    
    type counter struct {
        count    int
        resetAt  time.Time
    }
    
    type rateLimitByUserFilter struct {
        limit    int
        window   time.Duration
        counters map[string]*counter
        mu       sync.RWMutex
    }
    
    func (f *rateLimitByUserFilter) Request(ctx filters.FilterContext) {
        userID := ctx.Request().Header.Get("X-User-ID")
        if userID == "" {
            return
        }
        
        f.mu.Lock()
        defer f.mu.Unlock()
        
        c, exists := f.counters[userID]
        if !exists || time.Now().After(c.resetAt) {
            f.counters[userID] = &counter{
                count:   1,
                resetAt: time.Now().Add(f.window),
            }
            return
        }
        
        c.count++
        if c.count > f.limit {
            ctx.Serve(&http.Response{
                StatusCode: 429,
                Header: http.Header{
                    "Retry-After": []string{c.resetAt.Sub(time.Now()).String()},
                },
            })
        }
    }
    
    func (f *rateLimitByUserFilter) Response(ctx filters.FilterContext) {}
    Use custom filter in routes
    // Using the custom auth filter
    api: Path("/api/*")
        -> customAuth("my-secret-key")
        -> "http://backend:8080";
    
    // Using user-based rate limiting
    userApi: Path("/user/api/*")
        -> rateLimitByUser(100, "1m")
        -> "http://backend:8080";
    8

    Load Balancing

    Configure load balancing across multiple backend servers:

    Load balancing algorithms
    // Round-robin (default)
    roundRobin: Path("/api/*")
        -> <roundRobin, "http://server1:8080", "http://server2:8080", "http://server3:8080">;
    
    // Random selection
    randomLB: Path("/api/*")
        -> <random, "http://server1:8080", "http://server2:8080">;
    
    // Consistent hash (sticky by header)
    consistentHash: Path("/api/*")
        -> consistentHashKey("${request.header.X-User-ID}")
        -> <consistentHash, "http://server1:8080", "http://server2:8080">;
    
    // Power of two random choices
    powerOfTwo: Path("/api/*")
        -> <powerOfRandomNChoices, "http://server1:8080", "http://server2:8080", "http://server3:8080">;
    Health checks and failover
    // With health checks
    healthChecked: Path("/api/*")
        -> <roundRobin, 
            "http://server1:8080", 
            "http://server2:8080">
        -> healthcheck("/health", "3s", "2", "3");
    
    // Passive health checks (mark unhealthy on failures)
    passive: Path("/api/*")
        -> passiveHealthCheck(5, "30s")
        -> <roundRobin, "http://server1:8080", "http://server2:8080">;
    9

    Authentication

    Implement authentication using built-in filters:

    OAuth2/JWT authentication
    // OAuth2 token introspection
    oauth2: Path("/api/*")
        -> oauthTokenintrospectionAnyKV("scope", "read write")
        -> "http://backend:8080";
    
    // JWT validation
    jwt: Path("/api/*")
        -> jwtValidation("https://auth.example.com/.well-known/jwks.json")
        -> "http://backend:8080";
    
    // Forward auth (external auth service)
    forwardAuth: Path("/api/*")
        -> forwardToken("X-Auth-Token", "Authorization")
        -> "http://backend:8080";
    Basic authentication
    // Basic auth with htpasswd file
    basicAuth: Path("/admin/*")
        -> basicAuth("/etc/skipper/htpasswd")
        -> "http://admin-backend:8080";
    
    // Create htpasswd file
    # htpasswd -c /etc/skipper/htpasswd admin
    API key validation
    // Header-based API key
    apiKey: Path("/api/*") && Header("X-API-Key", "secret-key-123")
        -> "http://backend:8080";
    
    // Query parameter API key
    apiKeyQuery: Path("/api/*") && QueryParam("api_key", "secret-key-123")
        -> "http://backend:8080";
    
    // Reject unauthorized
    unauthorized: Path("/api/*")
        -> status(401)
        -> inlineContent("Unauthorized")
        -> <shunt>;
    10

    Metrics & Monitoring

    Skipper exposes Prometheus metrics for monitoring:

    Enable metrics
    # Start with Prometheus metrics
    skipper \
        -routes-file /etc/skipper/routes.eskip \
        -metrics-flavour prometheus \
        -enable-connection-metrics \
        -serve-host-metrics \
        -histogram-metric-buckets=0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10 \
        -support-listener :9911
    Prometheus scrape config
    scrape_configs:
      - job_name: 'skipper'
        static_configs:
          - targets: ['localhost:9911']
        metrics_path: /metrics

    Key Metrics

    skipper_serve_host_duration_seconds

    Request latency histogram per host

    skipper_route_duration_seconds

    Latency per route ID

    skipper_backend_errors_total

    Backend error count per route

    skipper_custom_total

    Custom counters from filters

    11

    Troubleshooting

    Common Issues

    Routes Not Loading
    • Validate syntax: skipper -routes-file routes.eskip -print-routes
    • Check for duplicate route IDs
    • Ensure file permissions are correct
    502 Bad Gateway
    • Verify backend is running and accessible
    • Check backend URL in route definition
    • Review timeout settings
    Kubernetes Ingress Not Working
    • Verify ServiceAccount permissions
    • Check Skipper logs: kubectl logs -n kube-system -l app=skipper-ingress
    • Ensure IngressClass is set correctly
    Useful debugging commands
    # View current routes
    curl http://localhost:9911/routes
    
    # Check health
    curl http://localhost:9911/kube-system/healthz
    
    # View metrics
    curl http://localhost:9911/metrics
    
    # Debug logging
    skipper -routes-file routes.eskip -application-log-level=DEBUG
    
    # Kubernetes logs
    kubectl logs -n kube-system -l app=skipper-ingress -f
    
    # Test route matching
    curl -H "Host: api.example.com" http://localhost:9090/test

    Ready to Deploy Skipper?

    Get started with a RamNode Cloud VPS and have Skipper routing traffic in minutes.