Files
homelab/ansible/roles/hermes_docker/tasks/main.yml
T
rblundon 881e2e24a8 Add shared nginx-proxy role; deploy Hermes in Docker; rename Paperclip
nginx-proxy (new role):
- Shared nginx + certbot under /opt/nginx, replacing the copies that
  lived inside the Paperclip compose project
- Apps publish via tasks_from: add_vhost (cert marker -> issue cert ->
  write vhost -> nginx -t -> reload); a vhost that fails nginx -t is
  rolled back
- Certs issued one per domain via Cloudflare DNS challenge; domains
  registered as marker files in cert-domains/; optional LE staging
- Stock nginx.conf plus conf.d (directory mounts, not single-file
  mounts); catch-all server drops unknown Host/SNI
- Containers renamed to nginx and certbot

hermes_docker (new role):
- Official Hermes image, host networking, gateway and dashboard bound
  to loopback and published through nginx-proxy
- Dashboard login via basic-auth provider, secrets from the vault
- Main model set from hermes_model (fantasmic endpoint, key from vault);
  config.yaml overrides are merged from a 0600 staged file that is
  removed afterwards, so the key never appears in a command line
- Data dir owner-only (HERMES_HOME_MODE=0700)

paperclip-docker:
- Remove nginx, certbot and docker-compose download tasks; deploy with
  community.docker.docker_compose_v2 (idempotent, remove_orphans)
- Stop passing the Cloudflare token and certbot email to the Paperclip
  container via env_file
- Bind Postgres to 127.0.0.1 (host networking exposed it on all
  interfaces); stop chowning pgdata, which the postgres image owns
- Handler recreates the paperclip service so .env changes apply
- Domain m-v.wdw-labs.cloud -> cranium-command.wdw-labs.cloud

Playbooks:
- Add day1_deploy_nginx_proxy.yml and day1_deploy_hermes_docker.yml
- deploy_paperclip_stack.yml: run nginx-proxy before paperclip-docker,
  CLI-based container checks (no Docker SDK dependency)
- Remove deploy-hermes.yml

Host vars: add nginx_proxy_certbot_email and nginx_proxy_certbot_staging
(staging stays on until the switch to real certs).
2026-09-30 23:12:40 -05:00

149 lines
4.7 KiB
YAML

---
- name: Preflight | Required vault variables are defined
ansible.builtin.assert:
that: "{{ item }} is defined and ({{ item }} | length) > 0"
fail_msg: "{{ item }} is not defined. Add it to your ansible vault."
quiet: true
loop: "{{ hermes_required_vault_vars }}"
tags: [hermes, preflight]
- name: Preflight | Confirm docker CLI is available
ansible.builtin.command: docker --version
changed_when: false
tags: [hermes, preflight]
- name: Preflight | Get existing container state
ansible.builtin.command: "docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ hermes_container_name }}"
register: hermes_existing
failed_when: false
changed_when: false
tags: [hermes, preflight]
# Only enforce free ports when our container is not already running,
# so redeploys stay idempotent.
- name: Preflight | Ensure required ports are free
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ item }}"
state: stopped
timeout: 3
loop: "{{ hermes_preflight_ports }}"
when: hermes_existing.stdout | default('') | trim != 'true'
tags: [hermes, preflight]
- name: Create project directory
ansible.builtin.file:
path: "{{ hermes_project_dir }}"
state: directory
mode: "0755"
tags: [hermes]
# No owner here: the image's start-up hook chowns the volume to the hermes user.
- name: Create data directory (owner-only)
ansible.builtin.file:
path: "{{ hermes_data_dir }}"
state: directory
mode: "{{ hermes_data_dir_mode }}"
tags: [hermes]
# Compose reads this as root on the host; the container never sees the file.
- name: Template secrets env file
ansible.builtin.template:
src: hermes-secrets.env.j2
dest: "{{ hermes_secrets_env_file }}"
owner: root
group: root
mode: "0600"
no_log: true
tags: [hermes]
- name: Template docker compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ hermes_project_dir }}/docker-compose.yml"
mode: "0644"
tags: [hermes]
- name: Deploy hermes
community.docker.docker_compose_v2:
project_src: "{{ hermes_project_dir }}"
pull: "{{ hermes_pull_policy }}"
state: present
tags: [hermes]
- name: Verify gateway health
ansible.builtin.uri:
url: "http://127.0.0.1:{{ hermes_gateway_port }}/health"
status_code: 200
register: hermes_health
until: hermes_health.status == 200
retries: "{{ (hermes_wait_timeout | int) // 5 }}"
delay: 5
when: hermes_api_server_enabled | bool
tags: [hermes, verify]
- name: Wait for dashboard port
ansible.builtin.wait_for:
host: "{{ hermes_dashboard_host }}"
port: "{{ hermes_dashboard_port }}"
timeout: "{{ hermes_wait_timeout }}"
when: hermes_dashboard_enabled | bool
tags: [hermes, verify]
# Overrides can contain secrets (the model api_key), so they are staged in a
# 0600 file owned by the container user instead of travelling in a command
# line (visible in `ps`), and removed again afterwards. The merge script runs
# inside the container with the image's own venv (PyYAML is available there),
# as the hermes user, so config.yaml stays owned by it.
- name: Apply config.yaml overrides
when: hermes_effective_overrides | length > 0
tags: [hermes, config]
block:
- name: Stage overrides file
ansible.builtin.copy:
content: "{{ hermes_effective_overrides | to_json }}"
dest: "{{ hermes_data_dir }}/.ansible-overrides.json"
owner: "{{ hermes_uid }}"
group: "{{ hermes_uid }}"
mode: "0600"
changed_when: false # transient file; only the merge result should count as a change
no_log: true
- name: Merge into config.yaml
ansible.builtin.command:
argv:
- docker
- exec
- -i
- --user
- hermes
- "{{ hermes_container_name }}"
- /opt/hermes/.venv/bin/python
- "-"
- /opt/data/.ansible-overrides.json
stdin: "{{ lookup('ansible.builtin.file', 'merge_config.py') }}"
register: hermes_config_merge
changed_when: "'CHANGED' in hermes_config_merge.stdout and 'UNCHANGED' not in hermes_config_merge.stdout"
notify: Restart hermes
always:
- name: Remove staged overrides file
ansible.builtin.file:
path: "{{ hermes_data_dir }}/.ansible-overrides.json"
state: absent
changed_when: false
- name: Publish dashboard through nginx-proxy
ansible.builtin.include_role:
name: nginx-proxy
tasks_from: add_vhost
vars:
vhost_name: hermes
vhost_domain: "{{ hermes_domain }}"
vhost_upstream_port: "{{ hermes_dashboard_port }}"
vhost_proxy_timeout: 3600s # long-lived dashboard WebSockets
when:
- hermes_publish_vhost | bool
- hermes_dashboard_enabled | bool
tags: [hermes, nginx]