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:
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
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:latestDocker Compose
The proxy repository ships a docker-compose.yml that starts the proxy together with Pocket ID:
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
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
productifyapp block withmanagerURL and{env.PFY_MANAGER_TOKEN}, the caddy-security OAuth2/OIDC portal, the Manager's own vhost) render frominstall.yamlat 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)
{
email admin@example.com
}
app.example.com {
# Automatic certificate
reverse_proxy backend:8080
}Custom Certificates
app.example.com {
tls /certs/cert.pem /certs/key.pem
reverse_proxy backend:8080
}Mount certificates:
volumes:
- ./certs:/certs:roMonitoring
Prometheus Metrics
The proxy exposes metrics on port 2112:
curl http://proxy:2112/metricsPrometheus Scrape Config
scrape_configs:
- job_name: "productify-proxy"
static_configs:
- targets: ["proxy:2112"]Available Metrics
pfy_authentication_awaiting_users- Users in authentication flowpfy_total_requests- Total requests per applicationpfy_response_time_seconds- Response time distributionpfy_request_size_bytes- Request size distributionpfy_response_size_bytes- Response size distribution
Health Checks
TCP Health Check
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:
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:
docker build --no-cache -t ghcr.io/productifyfw/proxy:latest ./proxyAuthentication 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:
curl https://idp.example.com/.well-known/openid-configurationManager Integration Errors
Check logs:
docker logs productify-proxyVerify:
- Manager URL is accessible from proxy
- Manager token is valid
- Application ID exists in Manager database
Test Manager connectivity:
# Manager health endpoint (health server port)
curl http://manager:8081/healthzCertificate 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:
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 InstancesShared State
Caddy stores certificate state in /data. For multiple instances:
- Use a shared volume (NFS, EFS)
- 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 offin production - Rate limiting - configure at load balancer level
- Security headers - add to Caddyfile