diff --git a/roles/devigo/README.md b/roles/devigo/README.md
new file mode 100644
index 0000000..f926482
--- /dev/null
+++ b/roles/devigo/README.md
@@ -0,0 +1,612 @@
+# Devigo Ansible Role
+
+Deploys the Devigo sales training website (devigo.no) to production infrastructure using Podman Quadlet and Caddy reverse proxy.
+
+## Description
+
+Devigo is a Hugo-based static marketing website deployed via a hybrid approach:
+- **Infrastructure**: Managed by Ansible (this role)
+- **Application**: Built and deployed via GitHub Actions CI/CD
+- **Runtime**: Podman Quadlet containers with systemd integration
+- **Web Server**: Caddy reverse proxy with automatic HTTPS
+
+This role deploys two services:
+1. **devigo-site**: The main website container (Hugo + Nginx)
+2. **devigo-decap-oauth**: OAuth2 proxy for Decap CMS content management
+
+## Architecture
+
+```
+GitHub Actions (CI/CD)
+ ↓
+Build Hugo site → Docker image → GHCR
+ ↓
+SSH to mini-vps → systemctl restart devigo-site
+ ↓
+Podman Quadlet pulls latest image
+ ↓
+Systemd manages container lifecycle
+ ↓
+Caddy reverse proxy (HTTPS)
+ ↓
+Internet (devigo.no)
+```
+
+## Requirements
+
+### Control Machine
+- Ansible 2.20+
+- Vault password for encrypted credentials
+- SSH access to production host
+
+### Target Host
+- Arch Linux (or compatible)
+- Podman 5.7+
+- Caddy with Cloudflare DNS plugin
+- Systemd with Quadlet support
+
+## Role Variables
+
+### Required Variables (set in group_vars/production/main.yml)
+
+```yaml
+# Domains
+devigo_domain: "devigo.no" # Apex domain (primary)
+devigo_www_domain: "www.devigo.no" # WWW subdomain (redirects to apex)
+devigo_primary_domain: "devigo.no" # Which domain is primary
+
+# OAuth Service
+devigo_oauth_domain: "decap.jnss.me" # OAuth proxy domain
+devigo_oauth_client_id: "{{ vault_devigo_oauth_client_id }}"
+devigo_oauth_client_secret: "{{ vault_devigo_oauth_client_secret }}"
+
+# IMPORTANT: Must be comma-separated string, NOT YAML array
+# The decapcms-oauth2 service requires TRUSTED_ORIGINS (plural) as a simple string
+devigo_oauth_trusted_origins: "https://devigo.no,https://www.devigo.no"
+
+# Container Registry
+devigo_ghcr_image: "ghcr.io/jnschaffer/rustan:prod"
+```
+
+### Optional Variables (defaults/main.yml)
+
+```yaml
+devigo_container_name: "devigo-site" # Container name
+devigo_container_port: 80 # Nginx port inside container
+devigo_oauth_user: "devigo-oauth" # OAuth service user
+devigo_oauth_home: "/opt/devigo-oauth" # OAuth config directory
+devigo_oauth_container_name: "devigo-decap-oauth"
+devigo_oauth_container_port: 12000 # OAuth service port
+```
+
+### Vault Variables (group_vars/production/vault.yml - encrypted)
+
+```yaml
+vault_devigo_oauth_client_id: "Ov23liNvuDONNB0dhwj6"
+vault_devigo_oauth_client_secret: "5dc4d17cfa7d550dd1b5ade0e33d1c9689bbbfad"
+```
+
+## Dependencies
+
+This role depends on:
+- `podman` - Container runtime
+- `caddy` - Reverse proxy with TLS
+
+These are automatically included via `meta/main.yml`.
+
+## Deployed Services
+
+### 1. devigo-site (Main Website)
+
+**Systemd Unit**: `devigo-site.service`
+**Quadlet File**: `/etc/containers/systemd/devigo-site.container`
+**Container**: `ghcr.io/jnschaffer/rustan:prod`
+**Network**: `caddy` (Podman network)
+**Health Check**: Verifies `/usr/share/nginx/html/index.html` exists
+
+**Features**:
+- Auto-update support via `podman auto-update`
+- Systemd managed (auto-restart on failure)
+- Health monitoring
+- Connected to Caddy network for reverse proxy
+
+### 2. devigo-decap-oauth (CMS OAuth Proxy)
+
+**Systemd Unit**: `devigo-decap-oauth.service`
+**Quadlet File**: `/etc/containers/systemd/devigo-decap-oauth.container`
+**Container**: `docker.io/alukovenko/decapcms-oauth2:latest`
+**Port**: `127.0.0.1:12000:12000`
+**Purpose**: OAuth2 proxy for Decap CMS GitHub authentication
+
+**Features**:
+- Enables content editing via Decap CMS at `/admin`
+- Authenticates with GitHub OAuth app
+- Proxied via Caddy at `decap.jnss.me`
+
+## Caddy Configuration
+
+### Site Configuration
+- **devigo.no** - Primary domain, serves content
+- **www.devigo.no** - Redirects to apex domain
+- Reverse proxy to `devigo-site` container via Podman network
+- Security headers (X-Frame-Options, CSP, etc.)
+- JSON logging to `/var/log/caddy/devigo.log`
+
+### OAuth Configuration
+- **decap.jnss.me** - OAuth proxy endpoint
+- Reverse proxy to `localhost:12000`
+- Required for Decap CMS `/admin` functionality
+
+## GitHub Actions Integration
+
+### Workflow Overview
+
+The site is deployed via GitHub Actions when code is pushed to the `prod` branch:
+
+1. **Build Phase**: Hugo site built in Docker container
+2. **Push Phase**: Image pushed to `ghcr.io/jnschaffer/rustan:prod`
+3. **Deploy Phase**: SSH to mini-vps, restart systemd service
+
+### Deploy Script (in .github/workflows/deploy.yml)
+
+```yaml
+deploy:
+ needs: build-and-push
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/prod'
+
+ steps:
+ - name: Deploy to mini-vps
+ uses: appleboy/ssh-action@v1.0.0
+ with:
+ host: ${{ secrets.VPS_HOST }}
+ username: ${{ secrets.VPS_USER }}
+ key: ${{ secrets.VPS_SSH_KEY }}
+ script: |
+ # Check for updates
+ podman auto-update --dry-run devigo-site
+
+ # Restart service to pull and use new image
+ systemctl restart devigo-site
+
+ # Verify deployment
+ sleep 5
+ systemctl is-active devigo-site
+ podman ps | grep devigo-site
+
+ # Cleanup
+ podman system prune -f
+```
+
+### Required GitHub Secrets
+
+Configure in repository settings (`https://github.com/jnschaffer/rustan/settings/secrets/actions`):
+
+- `VPS_HOST`: `72.62.91.251` (mini-vps IP)
+- `VPS_USER`: `root`
+- `VPS_SSH_KEY`: SSH private key for mini-vps access
+
+## Usage
+
+### Deploy Infrastructure
+
+```bash
+# Deploy all devigo infrastructure
+ansible-playbook playbooks/production.yml --tags devigo
+
+# Deploy all production services
+ansible-playbook playbooks/production.yml
+```
+
+### Manual Operations
+
+```bash
+# Check service status
+ansible mini-vps -m shell -a "systemctl status devigo-site"
+ansible mini-vps -m shell -a "systemctl status devigo-decap-oauth"
+
+# View logs
+ansible mini-vps -m shell -a "journalctl -u devigo-site -n 50"
+ansible mini-vps -m shell -a "journalctl -u devigo-decap-oauth -n 50"
+
+# Check containers
+ansible mini-vps -m shell -a "podman ps"
+
+# Manually restart services
+ansible mini-vps -m shell -a "systemctl restart devigo-site"
+ansible mini-vps -m shell -a "systemctl restart devigo-decap-oauth"
+
+# Check for available updates
+ansible mini-vps -m shell -a "podman auto-update --dry-run"
+
+# Force pull latest image
+ansible mini-vps -m shell -a "systemctl restart devigo-site"
+```
+
+### Testing Deployment
+
+```bash
+# Test site (after DNS update)
+curl -I https://devigo.no
+curl -I https://www.devigo.no # Should redirect to apex
+
+# Test OAuth proxy
+curl -I https://decap.jnss.me
+
+# Test from control machine with IP
+curl -I -k -H "Host: devigo.no" https://72.62.91.251
+```
+
+## Content Management
+
+### Decap CMS Access
+
+1. Navigate to `https://devigo.no/admin`
+2. Click "Login with GitHub"
+3. Authenticate via OAuth proxy (`decap.jnss.me`)
+4. Edit content via CMS interface
+5. Changes commit to GitHub → triggers rebuild → auto-deploy
+
+### Direct Git Workflow
+
+```bash
+cd ~/rustan
+# Make changes to content/norwegian/
+git add .
+git commit -m "Update content"
+git push origin prod
+# GitHub Actions builds and deploys automatically
+```
+
+## DNS Configuration
+
+Required DNS records:
+
+```
+devigo.no A 72.62.91.251
+www.devigo.no CNAME devigo.no.
+decap.jnss.me A 72.62.91.251
+```
+
+TLS certificates are automatically obtained via Caddy + Cloudflare DNS challenge.
+
+## Troubleshooting
+
+### Container Won't Start
+
+```bash
+# Check Quadlet file syntax
+ansible mini-vps -m shell -a "cat /etc/containers/systemd/devigo-site.container"
+
+# Check systemd status
+ansible mini-vps -m shell -a "systemctl status devigo-site"
+
+# View detailed logs
+ansible mini-vps -m shell -a "journalctl -u devigo-site -n 100"
+
+# Check if image exists
+ansible mini-vps -m shell -a "podman images | grep rustan"
+```
+
+### OAuth Not Working
+
+```bash
+# Verify OAuth container running
+ansible mini-vps -m shell -a "systemctl status devigo-decap-oauth"
+
+# Check OAuth logs
+ansible mini-vps -m shell -a "journalctl -u devigo-decap-oauth -n 50"
+
+# Test OAuth endpoint locally
+ansible mini-vps -m shell -a "curl -I http://localhost:12000"
+
+# Verify environment variables
+ansible mini-vps -m shell -a "cat /opt/devigo-oauth/decap-oauth.env"
+```
+
+### TLS Certificate Issues
+
+```bash
+# Check Caddy logs
+ansible mini-vps -m shell -a "journalctl -u caddy -n 100"
+
+# Verify certificate obtained
+ansible mini-vps -m shell -a "ls -la /var/lib/caddy/.local/share/caddy/certificates"
+
+# Force certificate renewal
+ansible mini-vps -m shell -a "systemctl reload caddy"
+```
+
+### GitHub Actions Deployment Fails
+
+1. Verify secrets are set in GitHub repository
+2. Check SSH connectivity: `ssh -i /path/to/key root@72.62.91.251`
+3. Verify image was pushed to GHCR: `https://github.com/jnschaffer/rustan/pkgs/container/rustan`
+4. Check GitHub Actions logs for detailed error messages
+
+## Automatic Updates (Future Enhancement)
+
+For fully automatic updates without GitHub Actions triggering:
+
+### Systemd Timer Approach
+
+Create a systemd timer to run `podman auto-update` daily:
+
+```ini
+# /etc/systemd/system/podman-auto-update.timer
+[Unit]
+Description=Podman Auto Update Timer
+
+[Timer]
+OnCalendar=daily
+Persistent=true
+
+[Install]
+WantedBy=timers.target
+```
+
+```ini
+# /etc/systemd/system/podman-auto-update.service
+[Unit]
+Description=Podman Auto Update Service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/podman auto-update
+ExecStartPost=/usr/bin/systemctl restart devigo-site
+```
+
+Enable timer:
+```bash
+systemctl enable --now podman-auto-update.timer
+```
+
+This provides automatic daily checks for new images independent of GitHub Actions.
+
+## Files Deployed
+
+```
+/etc/containers/systemd/
+├── devigo-site.container # Website Quadlet
+└── devigo-decap-oauth.container # OAuth Quadlet
+
+/etc/caddy/sites-enabled/
+├── devigo.caddy # Site reverse proxy config
+└── devigo-decap-oauth.caddy # OAuth reverse proxy config
+
+/opt/devigo-oauth/
+└── decap-oauth.env # OAuth environment variables
+
+/var/log/caddy/
+├── devigo.log # Site access logs
+└── devigo-decap-oauth.log # OAuth access logs
+```
+
+## Security Considerations
+
+1. **OAuth Credentials**: Stored encrypted in Ansible Vault
+2. **Container Security**: Runs with `NoNewPrivileges=true`
+3. **Network Isolation**: Containers only on `caddy` network
+4. **TLS**: All traffic encrypted via Cloudflare DNS challenge
+5. **SSH Keys**: GitHub Actions uses dedicated SSH key
+6. **Image Verification**: Images pulled from trusted GHCR
+
+## Notes
+
+- Hugo version on control machine (v0.152.2) is newer than package.json (v0.144.2) - builds work fine
+- Contact form uses Airform.io service (patrick@devigo.no endpoint)
+- Build time: ~3-5 seconds locally, ~30-60 seconds in GitHub Actions
+- Image size: ~50MB (Hugo + Nginx Alpine-based)
+- The role does not build the Hugo site - that's handled by GitHub Actions
+- DNS must point to mini-vps (72.62.91.251) before site will be accessible
+
+## Example Playbook
+
+```yaml
+---
+- name: Deploy Production Services
+ hosts: production
+ become: true
+ roles:
+ - role: devigo
+ tags: ['devigo', 'website', 'sales', 'oauth']
+```
+
+## License
+
+MIT
+
+## Author
+
+Created for rick-infra project by Claude Code
+
+## Troubleshooting
+
+### OAuth "Origin not trusted" Error
+
+**Symptom**: Browser console shows `Origin not trusted: https://devigo.no` when clicking "Login with GitHub"
+
+**Cause**: The `TRUSTED_ORIGINS` environment variable is incorrectly formatted or using the wrong variable name.
+
+**Solution**:
+1. Check the deployed env file:
+ ```bash
+ ansible mini-vps -m shell -a "cat /opt/devigo-oauth/decap-oauth.env"
+ ```
+
+2. Verify it contains:
+ ```
+ TRUSTED_ORIGINS=https://devigo.no,https://www.devigo.no
+ ```
+
+ **NOT**:
+ - `TRUSTED_ORIGIN=` (singular - only supports one origin)
+ - `TRUSTED_ORIGINS=['https://...']` (YAML array - incorrect format)
+
+3. If incorrect, ensure `roles/devigo/defaults/main.yml` contains:
+ ```yaml
+ devigo_oauth_trusted_origins: "https://devigo.no,https://www.devigo.no"
+ ```
+
+4. Redeploy and restart:
+ ```bash
+ ansible-playbook playbooks/production.yml --tags devigo
+ ansible mini-vps -m shell -a "systemctl restart devigo-decap-oauth"
+ ```
+
+### Decap CMS "Response was not yaml" Error
+
+**Symptom**: Decap CMS shows error: `Response for config.yml was not yaml. (Content-Type: application/octet-stream)`
+
+**Cause**: The `/admin/config.yml` file is not being served with `Content-Type: text/yaml`
+
+**Solution**:
+1. Verify nginx configuration in the container has the location block:
+ ```bash
+ ansible mini-vps -m shell -a "podman exec devigo-site grep -A 3 'location.*yml' /etc/nginx/nginx.conf"
+ ```
+
+2. Should contain:
+ ```nginx
+ location ~ \.(yml|yaml)$ {
+ default_type text/yaml;
+ }
+ ```
+
+3. Test the MIME type:
+ ```bash
+ curl -I https://devigo.no/admin/config.yml | grep content-type
+ # Expected: content-type: text/yaml
+ ```
+
+4. If missing, check the `rustan` repository's `nginx.conf` file and redeploy.
+
+### HTML/CSS Files Show as "application/octet-stream"
+
+**Symptom**: Site doesn't render correctly, browser shows files as `application/octet-stream`
+
+**Cause**: Server-level `types {}` block in nginx.conf overriding all MIME types
+
+**Solution**:
+1. Check nginx.conf in the container:
+ ```bash
+ ansible mini-vps -m shell -a "podman exec devigo-site cat /etc/nginx/nginx.conf | head -20"
+ ```
+
+2. Ensure there is **NO** `types { ... }` block at the server level
+3. YAML MIME type should be handled by location block only (see above)
+4. Fix in `rustan` repository, commit, and push to trigger rebuild
+
+### Cannot Pull Private GHCR Images
+
+**Symptom**: `podman pull` fails with 401 Unauthorized or authentication errors
+
+**Cause**: Missing or incorrect GitHub Container Registry authentication
+
+**Solution**:
+1. Verify auth.json exists:
+ ```bash
+ ansible mini-vps -m shell -a "ls -la /etc/containers/auth.json"
+ # Expected: -rw------- 1 root root
+ ```
+
+2. Test authentication:
+ ```bash
+ ansible mini-vps -m shell -a "podman login ghcr.io --get-login"
+ # Expected: jnschaffer (your GitHub username)
+ ```
+
+3. If fails, verify the Quadlet has the environment variable:
+ ```bash
+ ansible mini-vps -m shell -a "systemctl cat devigo-site.service | grep REGISTRY_AUTH_FILE"
+ # Expected: Environment=REGISTRY_AUTH_FILE=/etc/containers/auth.json
+ ```
+
+4. If missing, redeploy the role:
+ ```bash
+ ansible-playbook playbooks/production.yml --tags devigo,podman
+ ```
+
+### Service Won't Start After GitHub Actions Deployment
+
+**Symptom**: `systemctl status devigo-site` shows failed or inactive
+
+**Causes and Solutions**:
+
+1. **Image pull failed**:
+ ```bash
+ ansible mini-vps -m shell -a "journalctl -u devigo-site -n 50"
+ # Look for "401 Unauthorized" - see authentication troubleshooting above
+ ```
+
+2. **Port already in use**:
+ ```bash
+ ansible mini-vps -m shell -a "ss -tlnp | grep 9080"
+ # If occupied, stop conflicting service or change port
+ ```
+
+3. **Corrupted image**:
+ ```bash
+ ansible mini-vps -m shell -a "podman pull --force ghcr.io/jnschaffer/rustan:prod"
+ ansible mini-vps -m shell -a "systemctl restart devigo-site"
+ ```
+
+## Architecture Decisions
+
+### Why Apex Domain (devigo.no) Over WWW?
+
+**Decision**: Use `devigo.no` as primary, redirect `www.devigo.no` → `devigo.no`
+
+**Rationale**:
+- Shorter, cleaner URLs
+- Better for SEO (single canonical domain)
+- Simpler TLS certificate management
+- Modern web convention (www is legacy)
+
+### Why JavaScript Initialization for Decap CMS?
+
+**Decision**: Use `CMS.init()` with manual config loading instead of `` tag
+
+**Rationale**:
+- Eliminates dependency on YAML MIME type for `` element
+- More portable across different server configurations
+- Explicit and debuggable (clear in source what's happening)
+- Matches working configuration from previous deployment
+
+**Trade-off**: Slightly more verbose HTML (3 extra lines of JavaScript)
+
+### Why Location Block for YAML MIME Type?
+
+**Decision**: Use `location ~ \.(yml|yaml)$ { default_type text/yaml; }` instead of server-level `types {}` block
+
+**Rationale**:
+- Surgical approach - only affects YAML files
+- Doesn't override nginx's default MIME types for HTML/CSS/JS
+- Easier to understand and maintain
+- Prevents accidental breaking of other file types
+
+**Trade-off**: Requires location block in nginx.conf (but this is minimal)
+
+### Why Quadlet Over Docker Compose?
+
+**Decision**: Use Podman Quadlet with systemd integration instead of docker-compose
+
+**Rationale**:
+- Native systemd integration (journald logs, dependency management)
+- Better security (rootless capable, no daemon required)
+- Automatic service management via systemd generators
+- Auto-update support via `podman auto-update`
+- Aligns with Ansible infrastructure-as-code approach
+
+**Trade-off**: Less familiar to developers used to Docker Compose (but well-documented)
+
+## Related Documentation
+
+- [DEVIGO Deployment Guide](../../docs/devigo-deployment-guide.md) - Complete deployment walkthrough
+- [Podman Role](../podman/README.md) - Container runtime configuration
+- [Caddy Role](../caddy/README.md) - Reverse proxy and TLS
+- [Service Integration Guide](../../docs/service-integration-guide.md) - How services interact
+
+## License
+
+Part of the rick-infra project. See repository root for license information.
diff --git a/roles/devigo/defaults/main.yml b/roles/devigo/defaults/main.yml
new file mode 100644
index 0000000..d6f2510
--- /dev/null
+++ b/roles/devigo/defaults/main.yml
@@ -0,0 +1,30 @@
+---
+# Devigo Infrastructure - Default Variables
+
+# Domains
+devigo_domain: "devigo.no"
+devigo_www_domain: "www.devigo.no"
+devigo_primary_domain: "devigo.no" # Apex is primary
+
+# Container configuration
+devigo_container_name: "devigo-site"
+devigo_host_port: 9080 # Port published to localhost
+devigo_container_port: 80 # Nginx inside container
+
+# GitHub Container Registry
+devigo_ghcr_image: "ghcr.io/jnschaffer/rustan:prod"
+
+# Decap OAuth configuration
+devigo_oauth_domain: "decap.jnss.me"
+devigo_oauth_user: "devigo-oauth"
+devigo_oauth_home: "/opt/devigo-oauth"
+devigo_oauth_container_name: "devigo-decap-oauth"
+devigo_oauth_container_image: "docker.io/alukovenko/decapcms-oauth2:latest"
+devigo_oauth_container_port: 12000
+devigo_oauth_client_id: "{{ vault_devigo_oauth_client_id }}"
+devigo_oauth_client_secret: "{{ vault_devigo_oauth_client_secret }}"
+devigo_oauth_trusted_origins: "https://devigo.no,https://www.devigo.no"
+
+# Caddy integration (assumes caddy role provides these)
+# caddy_sites_enabled_dir: /etc/caddy/sites-enabled
+# caddy_user: caddy
diff --git a/roles/devigo/handlers/main.yml b/roles/devigo/handlers/main.yml
new file mode 100644
index 0000000..06adcad
--- /dev/null
+++ b/roles/devigo/handlers/main.yml
@@ -0,0 +1,19 @@
+---
+- name: reload systemd
+ systemd:
+ daemon_reload: yes
+
+- name: restart devigo-decap-oauth
+ systemd:
+ name: "{{ devigo_oauth_container_name }}"
+ state: restarted
+
+- name: restart devigo-site
+ systemd:
+ name: devigo-site
+ state: restarted
+
+- name: reload caddy
+ systemd:
+ name: caddy
+ state: reloaded
diff --git a/roles/devigo/meta/main.yml b/roles/devigo/meta/main.yml
new file mode 100644
index 0000000..de16f95
--- /dev/null
+++ b/roles/devigo/meta/main.yml
@@ -0,0 +1,4 @@
+---
+dependencies:
+ - role: podman
+ - role: caddy
diff --git a/roles/devigo/tasks/main.yml b/roles/devigo/tasks/main.yml
new file mode 100644
index 0000000..499617e
--- /dev/null
+++ b/roles/devigo/tasks/main.yml
@@ -0,0 +1,93 @@
+---
+# Devigo Deployment - Main Tasks
+
+# OAuth Service Setup
+- name: Create decap-oauth user
+ user:
+ name: "{{ devigo_oauth_user }}"
+ system: yes
+ shell: /usr/sbin/nologin
+ home: "{{ devigo_oauth_home }}"
+ create_home: yes
+
+- name: Create decap-oauth directories
+ file:
+ path: "{{ devigo_oauth_home }}"
+ state: directory
+ owner: "{{ devigo_oauth_user }}"
+ group: "{{ devigo_oauth_user }}"
+ mode: '0755'
+
+- name: Deploy OAuth environment file
+ template:
+ src: devigo-decap-oauth.env.j2
+ dest: "{{ devigo_oauth_home }}/decap-oauth.env"
+ owner: "{{ devigo_oauth_user }}"
+ group: "{{ devigo_oauth_user }}"
+ mode: '0600'
+ notify: restart devigo-decap-oauth
+
+- name: Create Quadlet systemd directory
+ file:
+ path: /etc/containers/systemd
+ state: directory
+ owner: root
+ group: root
+ mode: '0755'
+
+- name: Deploy Quadlet container file
+ template:
+ src: devigo-decap-oauth.container
+ dest: "/etc/containers/systemd/{{ devigo_oauth_container_name }}.container"
+ owner: root
+ group: root
+ mode: '0644'
+ register: quadlet_deployed
+
+- name: Reload systemd to discover Quadlet container
+ systemd:
+ daemon_reload: yes
+
+- name: Deploy OAuth Caddy configuration
+ template:
+ src: devigo-decap-oauth.caddy.j2
+ dest: "{{ caddy_sites_enabled_dir }}/devigo-decap-oauth.caddy"
+ owner: root
+ group: "{{ caddy_user }}"
+ mode: '0644'
+ notify: reload caddy
+
+- name: Enable and start decap-oauth service
+ systemd:
+ name: "{{ devigo_oauth_container_name }}"
+ enabled: yes
+ state: started
+
+# Devigo Site Quadlet Setup
+- name: Deploy devigo-site Quadlet container file
+ template:
+ src: devigo-site.container
+ dest: "/etc/containers/systemd/devigo-site.container"
+ owner: root
+ group: root
+ mode: '0644'
+ register: devigo_site_quadlet
+
+- name: Reload systemd to discover devigo-site Quadlet
+ systemd:
+ daemon_reload: yes
+
+- name: Deploy Caddy configuration for devigo site
+ template:
+ src: devigo.caddy.j2
+ dest: "{{ caddy_sites_enabled_dir }}/devigo.caddy"
+ owner: root
+ group: "{{ caddy_user }}"
+ mode: '0644'
+ notify: reload caddy
+
+- name: Enable and start devigo-site service
+ systemd:
+ name: devigo-site
+ enabled: yes
+ state: started
diff --git a/roles/devigo/tasks/setup_infrastructure.yml b/roles/devigo/tasks/setup_infrastructure.yml
new file mode 100644
index 0000000..e482271
--- /dev/null
+++ b/roles/devigo/tasks/setup_infrastructure.yml
@@ -0,0 +1,32 @@
+---
+# Set up Docker infrastructure for Devigo deployment
+
+- name: Create devigo deployment directory
+ file:
+ path: "{{ devigo_docker_dir }}"
+ state: directory
+ owner: root
+ group: root
+ mode: '0755'
+
+- name: Create caddy Docker network
+ containers.podman.podman_network:
+ name: caddy
+ state: present
+
+- name: Deploy docker-compose.yml
+ template:
+ src: docker-compose.yml.j2
+ dest: "{{ devigo_compose_file }}"
+ owner: root
+ group: root
+ mode: '0644'
+
+- name: Deploy Caddy configuration for devigo site
+ template:
+ src: devigo.caddy.j2
+ dest: "{{ caddy_sites_enabled_dir }}/devigo.caddy"
+ owner: root
+ group: "{{ caddy_user }}"
+ mode: '0644'
+ notify: reload caddy
diff --git a/roles/devigo/tasks/setup_oauth_service.yml b/roles/devigo/tasks/setup_oauth_service.yml
new file mode 100644
index 0000000..f57f9ba
--- /dev/null
+++ b/roles/devigo/tasks/setup_oauth_service.yml
@@ -0,0 +1,54 @@
+---
+# Set up Decap OAuth service
+
+- name: Create decap-oauth user
+ user:
+ name: "{{ devigo_oauth_user }}"
+ system: yes
+ shell: /usr/sbin/nologin
+ home: "{{ devigo_oauth_home }}"
+ create_home: yes
+
+- name: Create decap-oauth directories
+ file:
+ path: "{{ devigo_oauth_home }}"
+ state: directory
+ owner: "{{ devigo_oauth_user }}"
+ group: "{{ devigo_oauth_user }}"
+ mode: '0755'
+
+- name: Deploy OAuth environment file
+ template:
+ src: devigo-decap-oauth.env.j2
+ dest: "{{ devigo_oauth_home }}/decap-oauth.env"
+ owner: "{{ devigo_oauth_user }}"
+ group: "{{ devigo_oauth_user }}"
+ mode: '0600'
+ notify: restart devigo-decap-oauth
+
+- name: Deploy Quadlet container file
+ template:
+ src: devigo-decap-oauth.container
+ dest: "/etc/containers/systemd/{{ devigo_oauth_container_name }}.container"
+ owner: root
+ group: root
+ mode: '0644'
+ notify:
+ - reload systemd
+ - restart devigo-decap-oauth
+
+- name: Deploy OAuth Caddy configuration
+ template:
+ src: devigo-decap-oauth.caddy.j2
+ dest: "{{ caddy_sites_enabled_dir }}/devigo-decap-oauth.caddy"
+ owner: root
+ group: "{{ caddy_user }}"
+ mode: '0644'
+ notify: reload caddy
+
+- name: Enable and start decap-oauth service
+ systemd:
+ name: "{{ devigo_oauth_container_name }}"
+ enabled: yes
+ state: started
+ daemon_reload: yes
diff --git a/roles/devigo/templates/devigo-decap-oauth.caddy.j2 b/roles/devigo/templates/devigo-decap-oauth.caddy.j2
new file mode 100644
index 0000000..89eee33
--- /dev/null
+++ b/roles/devigo/templates/devigo-decap-oauth.caddy.j2
@@ -0,0 +1,25 @@
+# Decap CMS OAuth2 Proxy
+# Generated by Ansible - DO NOT EDIT MANUALLY
+
+{{ devigo_oauth_domain }} {
+ reverse_proxy 127.0.0.1:{{ devigo_oauth_container_port }}
+
+ # Security headers
+ header {
+ X-Frame-Options DENY
+ X-Content-Type-Options nosniff
+ Referrer-Policy strict-origin-when-cross-origin
+ }
+
+ # Logging
+ log {
+ output file /var/log/caddy/devigo-decap-oauth.log {
+ roll_size 100mb
+ roll_keep 5
+ }
+ format json {
+ time_format "2006-01-02T15:04:05.000Z07:00"
+ }
+ level INFO
+ }
+}
diff --git a/roles/devigo/templates/devigo-decap-oauth.container b/roles/devigo/templates/devigo-decap-oauth.container
new file mode 100644
index 0000000..075f318
--- /dev/null
+++ b/roles/devigo/templates/devigo-decap-oauth.container
@@ -0,0 +1,25 @@
+[Unit]
+Description=Decap CMS OAuth2 Proxy for Devigo
+After=network-online.target
+Wants=network-online.target
+
+[Container]
+Image={{ devigo_oauth_container_image }}
+ContainerName={{ devigo_oauth_container_name }}
+AutoUpdate=registry
+
+# Environment file with secrets
+EnvironmentFile={{ devigo_oauth_home }}/decap-oauth.env
+
+# Network
+PublishPort=127.0.0.1:{{ devigo_oauth_container_port }}:12000
+
+# Security
+NoNewPrivileges=true
+
+[Service]
+Restart=always
+TimeoutStartSec=900
+
+[Install]
+WantedBy=default.target
diff --git a/roles/devigo/templates/devigo-decap-oauth.env.j2 b/roles/devigo/templates/devigo-decap-oauth.env.j2
new file mode 100644
index 0000000..c306a2f
--- /dev/null
+++ b/roles/devigo/templates/devigo-decap-oauth.env.j2
@@ -0,0 +1,10 @@
+# Decap OAuth Environment Configuration
+# Generated by Ansible - DO NOT EDIT MANUALLY
+
+OAUTH_CLIENT_ID={{ devigo_oauth_client_id }}
+OAUTH_CLIENT_SECRET={{ devigo_oauth_client_secret }}
+SERVER_PORT=12000
+
+# TRUSTED_ORIGINS must be comma-separated string (not YAML array)
+# Example: https://example.com,https://www.example.com
+TRUSTED_ORIGINS={{ devigo_oauth_trusted_origins }}
diff --git a/roles/devigo/templates/devigo-site.container b/roles/devigo/templates/devigo-site.container
new file mode 100644
index 0000000..34856c3
--- /dev/null
+++ b/roles/devigo/templates/devigo-site.container
@@ -0,0 +1,31 @@
+[Unit]
+Description=Devigo Website - Sales Training Company
+After=network-online.target caddy.service
+Wants=network-online.target
+Requires=caddy.service
+
+[Container]
+Image=ghcr.io/jnschaffer/rustan:prod
+ContainerName=devigo-site
+AutoUpdate=registry
+Pull=newer
+
+# Port mapping - publish to localhost only
+PublishPort=127.0.0.1:9080:80
+
+# Security
+NoNewPrivileges=true
+
+# Health check - check if nginx is responding
+HealthCmd=/usr/bin/curl -f http://localhost:80/ || exit 1
+HealthInterval=30s
+HealthTimeout=10s
+HealthRetries=3
+
+[Service]
+Environment=REGISTRY_AUTH_FILE=/etc/containers/auth.json
+Restart=always
+TimeoutStartSec=900
+
+[Install]
+WantedBy=default.target
diff --git a/roles/devigo/templates/devigo.caddy.j2 b/roles/devigo/templates/devigo.caddy.j2
new file mode 100644
index 0000000..540cf76
--- /dev/null
+++ b/roles/devigo/templates/devigo.caddy.j2
@@ -0,0 +1,33 @@
+# Devigo Website - Reverse Proxy to Containerized Site
+# Generated by Ansible - DO NOT EDIT MANUALLY
+
+# Redirect www to apex (apex is primary per user preference)
+{{ devigo_www_domain }} {
+ redir https://{{ devigo_domain }}{uri} permanent
+}
+
+# Primary domain (apex)
+{{ devigo_domain }} {
+ reverse_proxy localhost:9080
+
+ # Security headers
+ header {
+ X-Frame-Options SAMEORIGIN
+ X-Content-Type-Options nosniff
+ X-XSS-Protection "1; mode=block"
+ Referrer-Policy strict-origin-when-cross-origin
+ Permissions-Policy "geolocation=(), microphone=(), camera=()"
+ }
+
+ # Logging
+ log {
+ output file /var/log/caddy/devigo.log {
+ roll_size 100mb
+ roll_keep 5
+ }
+ format json {
+ time_format "2006-01-02T15:04:05.000Z07:00"
+ }
+ level INFO
+ }
+}