Copier template for Keycloak SSO deployments on DigitalOcean droplets.
See docs/INFRA_BOOTSTRAP_PATTERN.md for
the shared pattern + 6-step migration checklist. This project's state today:
| Layer | Status | Notes |
|---|---|---|
| Layer 1 (DO Spaces remote state) | Done | template/terraform/backend.tf.jinja + template/terraform/init.sh.jinja. |
| Layer 2 (bootstrap-secret rotation) | Done | rotate-bootstrap-secret.sh embedded in template/terraform/templates/cloud-init.yaml.jinja. Logs in with v1, mints v2 via Infisical API, atomically swaps the auth file, revokes v1. |
| Path C (thin cloud-init + ansible) | Done | Cloud-init handles only first-boot bootstrap. template/ansible/deploy.yml.jinja owns compose + Caddyfile + backup script + cron + docker compose up. template/scripts/deploy.sh.jinja is a thin ansible-playbook wrapper. |
| Infisical CLI install | Current — uses artifacts-cli.infisical.com apt repo. |
This template provides a production-ready Keycloak SSO deployment with:
- Keycloak - Identity provider with OIDC/OAuth2 support
- Caddy - Reverse proxy with automatic TLS
- PostgreSQL - Persistent database
- OpenTofu - Infrastructure as Code (DigitalOcean droplets)
- Ansible - Server configuration management
- Infisical - Secrets management integration
keycloak-docker/
├── copier.yaml # Copier template configuration
├── README.md # This file
├── CHANGELOG.md # Changelog
└── template/
├── ansible/ # Ansible playbooks and roles
│ ├── inventories/ # Inventory files
│ ├── roles/ # Ansible roles
│ │ ├── common/ # Base server config
│ │ ├── docker/ # Docker installation
│ │ └── keycloak/ # Keycloak-specific config
│ ├── requirements.yml
│ └── site.yml
├── docker/ # Docker Compose files
│ ├── compose.local.yaml
│ ├── compose.prod.yaml
│ ├── Caddyfile
│ ├── Caddyfile.local
│ └── .env*.example
├── scripts/ # Deploy, backup, restore scripts
│ ├── deploy.sh
│ ├── backup.sh
│ └── restore.sh
└── terraform/ # OpenTofu infrastructure
├── main.tf
├── outputs.tf
├── variables.tf
├── monitoring.tf
├── versions.tf
└── templates/
└── cloud-init.yaml
# Install copier if not already installed
pip install copier
# Create a new Keycloak deployment
cd keycloak-docker
copier copy . ../keycloak-sso --data-file answers.yamlEdit answers.yaml with your specific values:
project_name: keycloak-sso
domain: sso.weown.ai
do_region: nyc3
droplet_size: s-2vcpu-4gb-amd
enable_infisical: true
infisical_project_id: your-infisical-project-idcd ../keycloak-sso/terraform
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your values
tofu init
tofu plan
tofu applycd ../scripts
chmod +x deploy.sh
./deploy.sh root@your-droplet-ipcd docker
cp .env.example .env.local
# Edit .env.local with your values
docker compose -f compose.local.yaml upSecrets are managed via Infisical using the bounce-to-refresh pattern.
What this achieves (ADR-006):
- Zero application secrets on disk — only the Machine Identity reaches the node (Layer 2 rotates even that on first boot); infra creds are injected as
TF_VAR_*byitofu.sh, never written toterraform.tfvars. - In-container secret fetch —
infisical runis the container entrypoint, fetching secrets in-process at every container start. Secrets are NOT in the composeenvironment:block, so they don't appear indocker inspect. - Bounce-to-refresh —
docker restartre-fetches secrets from Infisical (no redeploy needed). This enables consumer-side auto-rotation: rotate a secret in Infisical, bounce the container, it loads the new value. - Centralized management — edit a secret in Infisical; the next
docker restartpicks it up. - Multi-container secret duplication — PostgreSQL and Keycloak each see secrets under their expected env var names (e.g.,
POSTGRES_PASSWORDfor PostgreSQL,KC_DB_PASSWORDfor Keycloak). Same values, different names in Infisical.
To refresh secrets after rotating them in Infisical:
ssh root@your-droplet-ip
cd /opt/keycloak/data
docker compose restartIf Infisical Cloud becomes unavailable, deployments and backups will fail. See INFISICAL_OUTAGE_RUNBOOK.md for emergency procedures including:
- Manual deployment without Infisical
- Local-only backup creation
- Emergency restore procedures
- Recovery steps when Infisical comes back online
- OpenTofu: Re-running
tofu applyafter infrastructure exists will show no changes - Ansible: Re-running playbooks will only make necessary changes
- Deploy script: Re-running will only restart services if compose files changed
This Keycloak instance can provide SSO/OIDC/OAuth2 for:
- n8n (workflow automation)
- Nextcloud (file sync & collaboration)
- AnythingLLM (AI assistant / RAG)
- WordPress (CMS sites)
- Other internal services
See individual component licenses (Keycloak: Apache 2.0, PostgreSQL: PostgreSQL License)
Secrets reach this service at runtime via Infisical. The standard is documented in
docs/INFRA_BOOTSTRAP_PATTERN.md → Runtime secret injection
and .github/ADR-006-in-container-infisical-injection.md:
host-side infisical run wrap today (refresh on redeploy, not on a bare docker restart) →
moving toward in-container infisical run for bounce-to-refresh, with auto-reload, automatic
rotation, single-use tokens, and a clean K8s/K3s migration. No app secrets on disk or in git
(D247); only the project-scoped Machine Identity lives on the node.