Skip to content

Container Deployment

Run oidc-exchange as a long-lived container in ECS, EKS, Cloud Run, or any container orchestrator. The binary runs as an axum HTTP server when AWS_LAMBDA_RUNTIME_API is not set.

A runnable example is in examples/container/.

FROM rust:1.96-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
COPY . .
RUN cargo build --release --bin oidc-exchange
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/oidc-exchange /usr/local/bin/
COPY config/ /app/config/
WORKDIR /app
EXPOSE 8080
ENV OIDC_EXCHANGE_ENV=production
CMD ["oidc-exchange"]

The build needs pkg-config and libssl-dev because the HTTP client links the system OpenSSL. WORKDIR /app matters at runtime: the binary loads config from config/ relative to its working directory, so the process must run in the directory that holds the copied config/ tree. For a prebuilt image, the published ghcr.io/antstanley/oidc-exchange (pinned by digest) already handles all of this.

Create config/production.toml:

[server]
host = "0.0.0.0"
port = 8080
issuer = "https://auth.example.com"
[key_manager]
adapter = "local"
[key_manager.local]
private_key_path = "/etc/secrets/signing-key.pem"
algorithm = "EdDSA"
kid = "prod-1"
[repository]
adapter = "dynamodb"
[repository.dynamodb]
table_name = "oidc-exchange"
region = "us-east-1"
[audit]
adapter = "stdout"
durability = "enforce"
emit_threshold = "info"
[telemetry]
enabled = true
exporter = "otlp"
[providers.google]
adapter = "oidc"
issuer = "https://accounts.google.com"
client_id = "${GOOGLE_CLIENT_ID}"
client_secret = "${GOOGLE_CLIENT_SECRET}"
scopes = ["openid", "email", "profile"]
# Origins Google's discovery document may name beyond the issuer's origin:
endpoint_origins = ["https://oauth2.googleapis.com", "https://www.googleapis.com"]

endpoint_origins pins which origins a provider’s discovery document is allowed to name; each entry must be a bare https://host[:port], and an unpinned origin logs a warning when discovered (see Identity Providers).

Containers give you flexibility in key management:

  • Local keys: mount a signing key via a volume or Kubernetes secret. Use adapter = "local", which supports Ed25519 (EdDSA) only. ECDSA or RSA signing requires the KMS adapter.
  • AWS KMS: if running in AWS (ECS/EKS), use adapter = "kms" with IAM roles for service accounts or task roles.
services:
oidc-exchange:
build: .
ports:
- "8080:8080"
environment:
OIDC_EXCHANGE_ENV: production
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
AWS_REGION: us-east-1
volumes:
- ./keys:/etc/secrets:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
dynamodb-local:
image: amazon/dynamodb-local
ports:
- "8000:8000"

Build and push the image to ECR:

Terminal window
docker build -t oidc-exchange .
docker tag oidc-exchange:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/oidc-exchange:latest
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/oidc-exchange:latest

In your task definition, set the key configuration:

  • Use adapter = "kms" with a KMS key ARN and attach the appropriate IAM policy to the task role.
  • Alternatively, inject a local signing key via AWS Secrets Manager into the container environment.

For production-grade ECS Fargate with auto-scaling and ALB, see the dedicated ECS Fargate guide.

apiVersion: apps/v1
kind: Deployment
metadata:
name: oidc-exchange
spec:
replicas: 2
selector:
matchLabels:
app: oidc-exchange
template:
metadata:
labels:
app: oidc-exchange
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: oidc-exchange
image: your-registry/oidc-exchange@sha256:<pin-to-a-published-digest>
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
ports:
- containerPort: 8080
env:
- name: OIDC_EXCHANGE_ENV
value: production
- name: GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: oidc-exchange-secrets
key: google-client-id
- name: GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: oidc-exchange-secrets
key: google-client-secret
volumeMounts:
- name: signing-key
mountPath: /etc/secrets
readOnly: true
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
volumes:
- name: signing-key
secret:
secretName: oidc-exchange-signing-key
defaultMode: 0440
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: oidc-exchange
spec:
selector:
app: oidc-exchange
ports:
- port: 80
targetPort: 8080
type: ClusterIP

oidc-exchange is stateless (all state is in the configured database). Scale horizontally without coordination. Each instance holds an in-memory JWKS cache for upstream providers; this warms up on first request per provider and refreshes automatically.