Prerequisites & VPS Selection
💡 Tip: For Kubernetes deployments, see our Kubernetes deployment guide first to set up your cluster.
Install Skipper
Install Skipper using pre-built binaries or Docker:
Option 1: Binary Installation
# 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 -versionOption 2: Docker Installation
# 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.eskipCreate Systemd 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# 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 skipperBasic Configuration
Skipper uses eskip format for route definitions. Create your routes file:
// 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
# Validate routes file
skipper -routes-file /etc/skipper/routes.eskip -print-routes
# Reload routes (if running)
sudo systemctl reload skipperRoute Definitions
Advanced routing patterns for different use cases:
// 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";// 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";// 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";Built-in Filters
Skipper includes powerful built-in filters for common operations:
// 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 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";// Enable compression
compressed: Path("/api/*")
-> compress()
-> "http://backend:8080";
// Response caching
cached: Path("/static/*")
-> fifo(100, 50, "10s")
-> "http://static-backend:8080";// 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";Kubernetes Integration
Deploy Skipper as a Kubernetes Ingress Controller. See our Kubernetes guide for cluster setup.
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: 5apiVersion: 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: 9443apiVersion: 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-systemIngress with Skipper Annotations
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: 80RouteGroup CRD (Advanced)
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# 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-ingressCustom Filter Plugins
Create custom filters in Go to extend Skipper's functionality:
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
}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{},
},
})
}# 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.eskipExample: User-based Rate Limiting Filter
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) {}// 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";Load Balancing
Configure load balancing across multiple backend servers:
// 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">;// 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">;Authentication
Implement authentication using built-in filters:
// 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 auth with htpasswd file
basicAuth: Path("/admin/*")
-> basicAuth("/etc/skipper/htpasswd")
-> "http://admin-backend:8080";
// Create htpasswd file
# htpasswd -c /etc/skipper/htpasswd admin// 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>;Metrics & Monitoring
Skipper exposes Prometheus metrics for monitoring:
# 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 :9911scrape_configs:
- job_name: 'skipper'
static_configs:
- targets: ['localhost:9911']
metrics_path: /metricsKey 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
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
# 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/testReady to Deploy Skipper?
Get started with a RamNode Cloud VPS and have Skipper routing traffic in minutes.
