Skip to content

Proxy Deployment

Complete deployment guide for the Productify Proxy component (custom Caddy build with Productify plugins).

Deployment Overview

The Proxy uses a custom Caddy build with:

  • OAuth2/OIDC authentication (Caddy Security plugin)
  • Productify custom plugins for Manager integration
  • Automatic HTTPS with Let's Encrypt
  • Prometheus metrics on port 2112

Build Custom Caddy

The proxy requires custom Caddy plugins that must be built from source:

bash
cd proxy
docker build -t ghcr.io/productifyfw/proxy:latest .

The Dockerfile compiles:

  • Caddy Security plugin (OAuth2/OIDC)
  • Productify authentication plugins
  • Productify middleware plugins

Docker Deployment

Basic Deployment

bash
docker run -d \
  --name productify-proxy \
  -p 80:80 \
  -p 443:443 \
  -p 2112:2112 \
  -v $PWD/Caddyfile:/etc/caddy/Caddyfile \
  -v caddy_data:/data \
  -v caddy_config:/config \
  ghcr.io/productifyfw/proxy:latest

Docker Compose

The proxy repository ships a docker-compose.yml that starts the proxy together with Pocket ID:

yaml
volumes:
  pocketid:

networks:
  app:

services:
  pocketid:
    image: ghcr.io/pocket-id/pocket-id:v1
    env_file: .env.pocketid
    ports:
      - 1411:1411
    networks:
      - app
    volumes:
      - "pocketid:/app/data"

  proxy:
    build:
      context: .
      dockerfile: Dockerfile-local
    ports:
      - "80:80"
      - "443:443"
      - "2112:2112" # Prometheus metrics
    depends_on:
      - pocketid
    networks:
      - app
    volumes:
      - "./Caddyfile:/etc/caddy/Caddyfile"

The Manager URL and authentication token are configured in the Caddyfile's productify app block, not via environment variables.

Nomad Deployment

Job Specification

hcl
job "productify-proxy" {
  datacenters = ["dc1"]
  type        = "service"

  group "proxy" {
    count = 1

    network {
      port "http" {
        static = 80
        to     = 80
      }
      port "https" {
        static = 443
        to     = 443
      }
      port "metrics" {
        to = 2112
      }
    }

    service {
      name     = "productify-proxy"
      port     = "https"
      provider = "nomad"
      tags     = ["proxy", "https"]

      check {
        type     = "tcp"
        port     = "https"
        interval = "10s"
        timeout  = "2s"
      }
    }

    service {
      name     = "productify-proxy-metrics"
      port     = "metrics"
      provider = "nomad"
      tags     = ["metrics"]
    }

    task "caddy" {
      driver = "docker"

      config {
        image = "ghcr.io/productifyfw/proxy:latest"
        ports = ["http", "https", "metrics"]

        volumes = [
          "local/Caddyfile:/etc/caddy/Caddyfile",
        ]
      }

      template {
        data = <<EOF
{
  email admin@example.com
  admin off

  security {
    oauth identity provider generic {
      realm generic
      driver generic
      client_id <client-id>
      client_secret <client-secret>
      scopes openid email profile
      base_auth_url http://pocketid.localhost
      metadata_url http://pocketid.localhost/.well-known/openid-configuration
    }

    authentication portal pocketportal {
      crypto default token lifetime 3600
      enable identity provider generic
      cookie insecure off

      transform user {
        match realm generic
        action add role user
      }
    }

    authorization policy pocketpolicy {
      set auth url /auth/oauth2/generic
      allow roles user
      validate bearer header
      inject headers with claims
      enable strip token
    }
  }

  productify {
    manager http://172.17.0.1:8080
    token <manager-token>
  }
}

# Manager UI/API
manager.example.com {
  @auth {
    path /auth/*
  }

  route @auth {
    authenticate with pocketportal
  }

  route /* {
    authorize with pocketpolicy
    reverse_proxy http://172.17.0.1:8080
  }
}

# Applications
app.example.com {
  vars app_id <application-uuid>

  @auth {
    path /auth/*
  }

  route @auth {
    productify_before_auth with {vars.app_id}
    authenticate with pocketportal
  }

  route /* {
    authorize with pocketpolicy
    productify with {vars.app_id}
    reverse_proxy http://app-backend:8080
  }
}
EOF

        destination = "local/Caddyfile"
      }

      resources {
        cpu    = 200
        memory = 128
      }
    }
  }
}

Configuration

Caddyfile Template

See Caddyfile Configuration for complete examples.

Configuration Sources

The proxy is configured entirely through Caddy config — it does not read environment variables — and since the Wave 3 release train that config is rendered, not hand-written. The Caddyfile shown above is reference output: it is the shape the renderer produces. See Generated Artifacts.

  • Install-level parts (global options, the productify app block with manager URL and {env.PFY_MANAGER_TOKEN}, the caddy-security OAuth2/OIDC portal, the Manager's own vhost) render from install.yaml at platform-deploy time (pfy platform render).
  • Per-application vhosts (application ID, domains, backend upstream, the productify_* directive chain) render from the Manager's deployment data and are pushed to the running proxy through Caddy's admin API whenever an application or domain changes — with drift detection against the rendered hash (pfy proxy diff|sync).

The proxy's admin API must be enabled and reachable only on the internal network (authenticated) for the push to work — a change from the admin off posture shown in older single-file examples.

TLS Certificates

Let's Encrypt (Automatic)

nginx
{
  email admin@example.com
}

app.example.com {
  # Automatic certificate
  reverse_proxy backend:8080
}

Custom Certificates

nginx
app.example.com {
  tls /certs/cert.pem /certs/key.pem
  reverse_proxy backend:8080
}

Mount certificates:

yaml
volumes:
  - ./certs:/certs:ro

Monitoring

Prometheus Metrics

The proxy exposes metrics on port 2112:

bash
curl http://proxy:2112/metrics

Prometheus Scrape Config

yaml
scrape_configs:
  - job_name: "productify-proxy"
    static_configs:
      - targets: ["proxy:2112"]

Available Metrics

  • pfy_authentication_awaiting_users - Users in authentication flow
  • pfy_total_requests - Total requests per application
  • pfy_response_time_seconds - Response time distribution
  • pfy_request_size_bytes - Request size distribution
  • pfy_response_size_bytes - Response size distribution

Health Checks

TCP Health Check

hcl
check {
  type     = "tcp"
  port     = "https"
  interval = "10s"
  timeout  = "2s"
}

HTTP Health Check (Metrics Endpoint)

The metrics server on port 2112 can double as an HTTP health check target:

hcl
check {
  type     = "http"
  port     = "metrics"
  path     = "/metrics"
  interval = "10s"
  timeout  = "2s"
}

Troubleshooting

Build Failures

Check:

  • Go version (1.25+ required)
  • All module dependencies available
  • Network access to Go module proxy

Rebuild:

bash
docker build --no-cache -t ghcr.io/productifyfw/proxy:latest ./proxy

Authentication Not Working

Verify:

  • Identity provider is accessible from proxy
  • OAuth2 client credentials are correct
  • Metadata URL returns valid OIDC configuration
  • Cookie settings match environment

Test OIDC metadata:

bash
curl https://idp.example.com/.well-known/openid-configuration

Manager Integration Errors

Check logs:

bash
docker logs productify-proxy

Verify:

  • Manager URL is accessible from proxy
  • Manager token is valid
  • Application ID exists in Manager database

Test Manager connectivity:

bash
# Manager health endpoint (health server port)
curl http://manager:8081/healthz

Certificate Errors

Let's Encrypt:

  • DNS points to proxy
  • Ports 80/443 publicly accessible
  • Valid email configured
  • Not hitting rate limits

Custom certificates:

  • Certificate files exist and are readable
  • Certificate matches domain
  • Private key is not encrypted

High Availability

Multiple Instances

Deploy multiple proxy instances behind a load balancer:

hcl
group "proxy" {
  count = 3  # Run 3 instances

  # ... rest of config
}

Load Balancer Configuration

Use external load balancer (e.g., HAProxy, nginx) to distribute traffic:

Internet → Load Balancer → Proxy Instances

Shared State

Caddy stores certificate state in /data. For multiple instances:

  1. Use a shared volume (NFS, EFS)
  2. Or configure a shared Caddy storage backend so instances coordinate certificate management

Security Considerations

  • Disable admin API in production (admin off)
  • Use HTTPS for all production domains
  • Secure Manager token - use secrets management
  • Cookie security - set cookie insecure off in production
  • Rate limiting - configure at load balancer level
  • Security headers - add to Caddyfile

See Also