Docker Compose Deployment
This guide covers deploying Inmor using Docker Compose for production environments.
Danger
Security Critical: Protect the Admin API
The Admin API (port 8000) provides full management access to the Trust Anchor, including the ability to:
Create and revoke trust marks
Add and remove subordinate entities
Modify the Trust Anchor’s entity configuration
In production, you MUST secure the Admin API behind authentication. At minimum, use HTTP Basic Authentication at the reverse proxy level. Consider additional measures such as:
IP allowlisting (restrict to management network)
VPN-only access
Client certificate authentication (mTLS)
OAuth2/OIDC authentication
Never expose the Admin API directly to the internet without authentication.
See Reverse Proxy Configuration for configuration examples.
Architecture
The Docker Compose setup includes 5 services:
ta - Trust Anchor (Rust) on port 8080
admin - Admin Portal (Django) on port 8000
db - PostgreSQL 14 database
redis - Redis 7 for caching and federation data
frontend - UI for admin work on port 5173
services:
ta:
image: docker.sunet.se/inmor:0.3.0
ports:
- "8080:8080"
depends_on:
redis:
condition: service_healthy
admin:
image: docker.sunet.se/inmor-admin:0.3.0
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ta:
condition: service_healthy
db:
image: postgres:14-alpine
ports:
- "5432:5432"
redis:
image: redis:7-alpine
frontend:
image: docker.sunet.se/inmor-frontend:0.3.0
depends_on:
- admin
ports:
- "127.0.0.1:5173:80"
Production Deployment
For production, you typically deploy behind a reverse proxy (nginx, Apache, Traefik) that handles TLS termination. The internal services communicate over HTTP.
In this example, the TA is running as https://realta.labb.sunet.se , we have separate subdomain for TA admin frontend and API service.
1. Create production configuration
Create a docker-compose.prod.yml override:
services:
ta:
ports:
- "127.0.0.1:8080:8080" # Only bind to localhost
environment:
- RUST_LOG=info
healthcheck:
test: ['CMD', 'curl', '--fail', '--silent', 'http://localhost:8080/health']
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
volumes:
- ./taconfig.toml:/app/taconfig.toml
- ./private.json:/app/private.json
- ./publickeys:/app/publickeys
- ./historical_keys:/app/historical_keys
- ./templates:/app/templates
admin:
ports:
- "127.0.0.1:8000:8000" # Only bind to localhost
command: /app/docker-entrypoint.sh
environment:
- INSIDE_CONTAINER=true
- CORS_ORIGINS=https://taui.labb.sunet.se,https://taapi.labb.sunet.se,http://localhost:5173
- CSRF_TRUSTED_ORIGINS=https://taui.labb.sunet.se,https://taapi.labb.sunet.se
- LOGIN_REDIRECT_URL=https://taui.labb.sunet.se/
volumes:
- ./localsettings.py:/app/inmoradmin/localsettings.py
- ./private.json:/app/private.json
- ./publickeys:/app/publickey
- ./historical_keys:/app/historical_keys
db:
ports: [] # Don't expose to host
environment:
- POSTGRES_PASSWORD=your_secure_password
2. Create production settings
Create localsettings.py for Django:
# Production settings override
DEBUG = False
ALLOWED_HOSTS = ['your-domain.example.com', 'localhost', '127.0.0.1', 'admin', 'taapi.labb.sunet.se', 'taui.labb.sunet.se']
SECRET_KEY = 'your-production-secret-key'
# Use your production domain
TA_DOMAIN = 'https://realta.labb.sunet.se'
# Add the required endpoints for TA
FEDERATION_ENTITY = {
"federation_fetch_endpoint": f"{TA_DOMAIN}/fetch",
"federation_list_endpoint": f"{TA_DOMAIN}/list",
"federation_resolve_endpoint": f"{TA_DOMAIN}/resolve",
"federation_trust_mark_status_endpoint": f"{TA_DOMAIN}/trust_mark_status",
"federation_trust_mark_list_endpoint": f"{TA_DOMAIN}/trust_mark_list",
"federation_trust_mark_endpoint": f"{TA_DOMAIN}/trust_mark",
"federation_historical_keys_endpoint": f"{TA_DOMAIN}/historical_keys",
"federation_collection_endpoint": f"{TA_DOMAIN}/collection",
}
TRUSTMARK_PROVIDER = 'https://federation.your-domain.example.com'
# Reverse proxy settings (REQUIRED — see section below)
SECURE_SSL_REDIRECT = False
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Trust marks issued to the TA itself
TA_TRUSTMARKS = [
{
"trust_mark_type": "https://your-domain.example.com/trustmark/member",
"mark": "eyJ...<JWT>..." # Pre-issued trust mark JWT
}
]
# Trusted trust mark issuers
TA_TRUSTED_TRUSTMARK_ISSUERS = {
"https://other-ta.example.com/trustmark/verified": [
"https://other-ta.example.com"
]
}
3. Create production taconfig.toml
Update taconfig.toml for production:
domain = "https://federation.your-domain.example.com"
redis_uri = "redis://redis:6379"
# TLS is handled by reverse proxy, so these can be self-signed or omitted
# tls_cert = "cert.pem"
# tls_key = "key.pem"
# SSRF protection: do NOT set allow_http in production.
# When omitted (or false), all outbound federation requests enforce HTTPS
# and reject targets that resolve to private/loopback IP ranges.
# allow_http = false
4. Deploy with production compose
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Running Behind a Reverse Proxy
In production, a reverse proxy (nginx, Caddy, Apache) terminates TLS and
forwards plain HTTP to the Django admin service. Without the correct Django
settings, this causes 301 redirects on POST requests — Django’s
SecurityMiddleware sees an http:// request and redirects to https://,
which drops the POST body.
Two settings in localsettings.py are required:
# Do NOT let Django redirect to HTTPS — the reverse proxy handles that.
SECURE_SSL_REDIRECT = False
# Trust the X-Forwarded-Proto header set by the reverse proxy so Django
# knows the original request was HTTPS (needed for CSRF, secure cookies, etc.)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
The reverse proxy must set the X-Forwarded-Proto header. Example for nginx:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
See Reverse Proxy Configuration for complete nginx, Apache, and Caddy configurations.
Volume Mounts
The following volumes should be mounted for production:
Trust Anchor (ta)
Volume |
Description |
|---|---|
|
TA configuration file |
|
Primary signing key |
|
Public keys directory |
|
Historical/expired keys |
Admin Portal (admin)
Volume |
Description |
|---|---|
|
Admin signing key |
|
Public keys directory |
|
Historical keys for JWT generation |
|
Production settings override |
Database Persistence
The PostgreSQL database stores:
Subordinate entities and their statements
Trust mark types and issued trust marks
Entity metadata and configurations
Mount a persistent volume:
services:
db:
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Database Backup
Backup the database regularly:
docker compose exec db pg_dump -U postgres postgres > backup.sql
Restore from backup:
docker compose exec -T db psql -U postgres postgres < backup.sql
Redis Data
Redis stores the federation runtime data:
Entity configurations (
inmor:entity_id)Subordinate statements (
inmor:subordinates)Trust marks by entity (
inmor:tm:{domain})Trust mark type memberships (
inmor:tmtype:{type})Entity type sets (
inmor:rp,inmor:op,inmor:taia)Collection data (
inmor:collection:*) — populated byinmor-collection
Redis data is ephemeral and can be rebuilt from the database:
# Rebuild entity configuration
curl -X POST http://localhost:8000/api/v1/server/entity
# Rebuild historical keys
curl -X POST http://localhost:8000/api/v1/server/historical_keys
# Reload trust marks from database
docker compose exec admin python manage.py reload_issued_tms
# Rebuild collection data (walks the federation tree)
docker compose exec ta ./inmor-collection -c taconfig.toml https://ta.example.com
Entity Collection CLI (inmor-collection)
The inmor-collection binary walks a federation tree starting from a trust anchor,
discovers all subordinate entities, and stores their collection data in Redis.
The /collection endpoint on the Trust Anchor reads this data.
Usage:
inmor-collection -c <config-file> <trust-anchor-entity-id>
Arguments:
Argument |
Description |
|---|---|
|
Path to |
|
Entity ID of the trust anchor to walk (e.g., |
Example:
# Run inside the Docker container
docker compose exec ta ./inmor-collection -c taconfig.toml https://ta.example.com
# With debug logging
docker compose exec ta sh -c 'RUST_LOG=debug ./inmor-collection -c taconfig.toml https://ta.example.com'
How it works:
Connects to Redis using the URI from
taconfig.tomlFetches the trust anchor’s entity configuration and extracts its trust mark recognition maps (
trust_mark_issuers/trust_mark_owners)Recursively discovers all subordinates by following
federation_list_endpointlinksFor each entity, extracts entity types, UI info (display name, logo, policy URI), and trust marks
Verifies each entity’s trust marks against the trust anchor and indexes the verified ones by trust mark type, so the
/collectiontrust_mark_typefilter matches only verified marksWrites all data to staging Redis keys (
inmor:collection:staging:*) during the walkOn completion, atomically swaps staging keys to live keys using a Redis RENAME pipeline
The staging-to-live swap ensures the /collection endpoint never serves partial data
during a walk.
Redis keys populated:
Key |
Type |
Content |
|---|---|---|
|
Hash |
entity_id → JSON entity object |
|
Set |
entity_ids of that type |
|
Set |
entity_ids with a verified trust mark of that type |
|
Set |
registry of indexed trust mark types (enumerates the |
|
String |
Entity Identifier the collection was walked from |
|
ZSet |
entity_ids for ordering |
|
String |
Unix timestamp of last walk |
Scheduling:
The tool is designed to run periodically via cron or systemd-timer. See Scheduled Tasks for cron and systemd timer configuration examples.
Scheduled Tasks
Inmor requires three periodic tasks for production operation:
Entity configuration regeneration — keeps the TA’s entity statement up to date (e.g. after adding subordinates, trust marks, or changing metadata via the Admin portal)
Subordinate renewal — re-fetches and re-verifies all active subordinate entity configurations, regenerates signed statements, and updates the database and Redis
Collection walk — discovers all entities in the federation tree and populates the
/collectionendpoint
Entity Configuration Regeneration
The regenerate_entity management command regenerates the Trust Anchor’s entity
configuration JWT and stores it in Redis. Run this periodically so that changes made
via the Admin portal (new subordinates, trust marks, metadata) are reflected in the
entity statement served at /.well-known/openid-federation.
Manual run:
# Via just
just regenerate-entity
# Or directly
docker compose exec admin python manage.py regenerate_entity
Cron (recommended for production):
# Run every minute
* * * * * cd /path/to/inmor && /usr/bin/docker compose exec -T admin python manage.py regenerate_entity >> /tmp/inmor-regenerate.log 2>&1
Systemd timer (alternative):
Create ~/.config/systemd/user/inmor-regenerate-entity.service:
[Unit]
Description=Regenerate inmor Trust Anchor entity configuration
[Service]
Type=oneshot
WorkingDirectory=/path/to/inmor
ExecStart=/usr/bin/docker compose exec -T admin python manage.py regenerate_entity
Create ~/.config/systemd/user/inmor-regenerate-entity.timer:
[Unit]
Description=Regenerate inmor entity configuration every minute
[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer:
systemctl --user daemon-reload
systemctl --user enable --now inmor-regenerate-entity.timer
# Ensure timers survive logout/reboot
loginctl enable-linger $(whoami)
Subordinate Renewal
The renew_subordinates management command renews all active subordinates by
re-fetching and verifying their entity configurations, regenerating signed
subordinate statements, and updating both the database and Redis. Each
subordinate is processed independently — a failure on one does not stop the
others.
Manual run:
docker compose exec admin python manage.py renew_subordinates
Cron (recommended for production):
# Run every 5 minutes
*/5 * * * * cd /path/to/inmor && /usr/bin/docker compose exec -T admin python manage.py renew_subordinates >> /tmp/inmor-renew-subordinates.log 2>&1
Systemd timer (alternative):
Create ~/.config/systemd/user/inmor-renew-subordinates.service:
[Unit]
Description=Renew all active inmor subordinates
[Service]
Type=oneshot
WorkingDirectory=/path/to/inmor
ExecStart=/usr/bin/docker compose exec -T admin python manage.py renew_subordinates
Create ~/.config/systemd/user/inmor-renew-subordinates.timer:
[Unit]
Description=Renew inmor subordinates every 5 minutes
[Timer]
OnCalendar=*-*-* *:*:00/5
Persistent=true
[Install]
WantedBy=timers.target
Enable:
systemctl --user daemon-reload
systemctl --user enable --now inmor-renew-subordinates.timer
Collection Walk Scheduling
See Entity Collection CLI (inmor-collection) above for details on inmor-collection.
Cron:
# Run every 5 minutes
*/5 * * * * cd /path/to/inmor && /usr/bin/docker compose exec -T ta ./inmor-collection -c taconfig.toml https://ta.example.com >> /tmp/inmor-collection.log 2>&1
Systemd timer:
Create ~/.config/systemd/user/inmor-collection.service:
[Unit]
Description=Walk federation tree and populate collection data
[Service]
Type=oneshot
WorkingDirectory=/path/to/inmor
ExecStart=/usr/bin/docker compose exec -T ta ./inmor-collection -c taconfig.toml https://ta.example.com
Create ~/.config/systemd/user/inmor-collection.timer:
[Unit]
Description=Run inmor collection walk every 5 minutes
[Timer]
OnCalendar=*-*-* *:*:00/5
Persistent=true
[Install]
WantedBy=timers.target
Enable:
systemctl --user daemon-reload
systemctl --user enable --now inmor-collection.timer
Health Checks
All services have health checks configured:
ta:
GET /health— verifies Redis connectivity, returns{"status": "ok"}(200) or{"status": "error", "detail": "redis unavailable"}(503)admin: Django application health
db: PostgreSQL ready check (
pg_isready)redis: Redis ping
The TA /health endpoint is used as the Docker healthcheck:
healthcheck:
test: ['CMD', 'curl', '--insecure', '--fail', '--silent', 'https://localhost:8080/health']
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
For detailed operational status (subordinate counts, trust mark types, collection stats),
use the /status endpoint:
curl https://your-ta-domain/status
Example response:
{
"entity_id": "https://federation.example.com",
"version": "0.3.0",
"status": "ok",
"keys": {
"public_keys": 3,
"historical_keys_available": true
},
"subordinates": {
"direct": 4
},
"trust_marks": {
"types": [
"https://example.com/trustmark/member",
"https://example.com/trustmark/certified"
],
"total_issued": 89
},
"collection": {
"total_entities": 523,
"openid_providers": 150,
"openid_relying_parties": 300,
"intermediates": 10,
"last_updated": 1708420000
}
}
Monitor health status:
docker compose ps
docker compose logs --tail=100
Scaling Considerations
For high-availability deployments:
Redis: Use Redis Cluster or Redis Sentinel
PostgreSQL: Use PostgreSQL replication or managed service
Trust Anchor: Can be scaled horizontally behind a load balancer
Admin Portal: Can be scaled horizontally, ensure shared Redis/PostgreSQL
Environment Variables
Trust Anchor (ta)
Variable |
Description |
|---|---|
|
Log level (debug, info, warn, error) |
Admin Portal (admin)
Variable |
Description |
|---|---|
|
Set to |
|
PostgreSQL host (default: |
|
PostgreSQL port (default: |
|
Redis URL (default: |
|
Path to historical keys (default: |
Generating the Signing Key
The Trust Anchor signs every entity statement and trust mark with the private
key in private.json and publishes the matching public keys from
publickeys/. The inmor-keygeneration binary, bundled in the TA image,
creates such a keypair without needing this source tree — run it once before
starting the server for the first time.
Ownership matters
The TA container runs as the image’s unprivileged app user, whose uid and
gid are pinned to 999 in the Dockerfile. private.json is created
with mode 0600 (readable only by its owner), so for the TA to read its
signing key, private.json must be owned by uid 999. The public key files
are written 0644, so their ownership does not matter.
The simplest way to satisfy this is to generate the keys as uid 999, which is the image’s default user. Create an output directory that uid 999 can write to, then run the generator:
mkdir -p keys
sudo chown 999:999 keys
docker run --rm -v ./keys:/data docker.sunet.se/inmor:0.4.0 \
/app/inmor-keygeneration --type=RS256 --output=/data
This writes two files into the keys directory:
private.json— the signing key, mode0600, owned by uid 999publickeys/{kid}.json— the public JWK, mode0644, named by its key ID
Mount them into the ta service read-only (see Volume Mounts):
volumes:
- ./keys/private.json:/app/private.json:ro
- ./keys/publickeys:/app/publickeys:ro
The --type option selects the key algorithm, following
RFC 9864 Section 2:
|
Key |
|---|---|
|
RSA 2048-bit, RSASSA-PKCS1-v1_5 |
|
RSA 2048-bit, RSASSA-PSS |
|
EC, curves P-256 / P-384 / P-521 |
|
Edwards curve (OKP) |
If private.json already exists, the command refuses to overwrite it; pass
--force to replace it.
Warning
Overwriting private.json invalidates every entity statement and trust
mark the Trust Anchor has already signed. Generate a new signing key only
for a fresh deployment or a deliberate key rotation.
Note
If you cannot chown a directory to uid 999 (for example on a host where
you lack root), generate the files as your own host user by adding
--user "$(id -u):$(id -g)" to the docker run command. The key files
are then owned by your host uid, so you must also add a matching user:
entry to the ta service in the compose file so the TA container runs
under the same uid and can read private.json.
Initialization Workflow
After deployment, initialize the Trust Anchor:
Create entity configuration:
curl -X POST http://localhost:8000/api/v1/server/entity
This creates the TA’s self-signed entity statement and stores it in Redis.
Create historical keys JWT (if you have rotated keys):
curl -X POST http://localhost:8000/api/v1/server/historical_keys
This creates a signed JWT containing all expired keys from
historical_keys/.Create trust mark types:
curl -X POST http://localhost:8000/api/v1/trustmarktypes \ -H "Content-Type: application/json" \ -d '{ "tmtype": "https://your-domain.example.com/trustmark/member", "valid_for": 8760, "autorenew": true }'
Add subordinates as they register with your Trust Anchor.
Logs and Monitoring
View service logs:
# All services
docker compose logs -f
# Specific service
docker compose logs -f ta
docker compose logs -f admin
For production monitoring, consider:
Prometheus metrics export
Centralized logging (ELK stack, Loki)
Alerting on health check failures