From a8545fc501c1bdc9a86f2097c277b8b138f36c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Fri, 29 May 2026 21:49:13 +0200 Subject: [PATCH] fix(podman): use Type=notify + service-container so systemd sees pod crashes The previous Type=oneshot + RemainAfterExit=true pattern made systemd freeze pod units in 'active (exited)' as soon as 'podman play kube' returned, so crash-looping containers were invisible to 'systemctl --user --failed' and Restart=on-failure never fired. For every podman-pod role (immich, fdroid, ntfy, gitea, qfieldcloud, unifi, matrix, uptime_kuma): - switch units to Type=notify + NotifyAccess=all - run 'podman kube play --service-container=true' so the unit's main PID stays alive as long as the pod - use 'podman kube down' for ExecStop - add TimeoutStartSec=180 to cover slow first-boot image pulls Pod manifests: flip every container's restartPolicy from Always to Never. systemd is now the single owner of the restart loop: container exits -> pod dies -> service container dies -> unit fails -> Restart=on-failure restarts everything cleanly. With Always, podman retried internally and hid the failure from systemd. CLAUDE.md updated to document the new canonical template and the 'restartPolicy: Never' requirement. --- CLAUDE.md | 357 ++++++++++++++++++ roles/fdroid/templates/fdroid.service.j2 | 15 + roles/fdroid/templates/fdroid.yaml.j2 | 53 +++ roles/gitea/templates/gitea.service.j2 | 9 +- roles/gitea/templates/gitea.yaml.j2 | 2 +- roles/immich/templates/immich.service.j2 | 9 +- roles/immich/templates/immich.yaml.j2 | 4 +- roles/matrix/templates/matrix.service.j2 | 17 + roles/matrix/templates/matrix.yaml.j2 | 63 ++++ roles/ntfy/templates/ntfy.service.j2 | 9 +- roles/ntfy/templates/ntfy.yaml.j2 | 2 +- .../templates/qfieldcloud.service.j2 | 15 + .../qfieldcloud/templates/qfieldcloud.yaml.j2 | 126 +++++++ roles/unifi/templates/unifi.service.j2 | 15 + roles/unifi/templates/unifi.yaml.j2 | 38 ++ .../templates/uptime-kuma.service.j2 | 9 +- .../uptime_kuma/templates/uptime-kuma.yaml.j2 | 2 +- 17 files changed, 724 insertions(+), 21 deletions(-) create mode 100644 CLAUDE.md create mode 100644 roles/fdroid/templates/fdroid.service.j2 create mode 100644 roles/fdroid/templates/fdroid.yaml.j2 create mode 100644 roles/matrix/templates/matrix.service.j2 create mode 100644 roles/matrix/templates/matrix.yaml.j2 create mode 100644 roles/qfieldcloud/templates/qfieldcloud.service.j2 create mode 100644 roles/qfieldcloud/templates/qfieldcloud.yaml.j2 create mode 100644 roles/unifi/templates/unifi.service.j2 create mode 100644 roles/unifi/templates/unifi.yaml.j2 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2665269 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,357 @@ +# Project Architecture & Patterns + +## Limit documentation + +- Only one README per role and a general project README. +- Default vars are already documented in ./roles//defaults/main.yml, no need to document them elsewhere. A link to this file from ./roles//README.md is sufficient. + +## Target Audience + +This repository targets **power users, developers, and homelab enthusiasts** who are comfortable with: + +- Command-line interfaces and SSH +- Basic networking and Linux system administration +- Reading technical documentation and following references +- Ansible concepts (roles, playbooks, inventory) + +**Documentation Philosophy:** + +- Straight to the point, no hand-holding +- Minimal redundancy - references between documents preferred +- Assumes familiarity with underlying technologies +- Technical accuracy over verbosity + +## Overview + +This Ansible repository manages NAS/homelab infrastructure with a focus on: + +- **Shared services pattern**: System-level PostgreSQL, Valkey, Nginx +- **Service isolation**: Separate databases and users per service +- **Security first**: Localhost-only access, minimal privileges, fail-fast validation +- **Modularity**: Independent service deployments + +## Key Architectural Decisions + +### 1. Shared Services Pattern + +**Why:** Resource efficiency, easier maintenance, better isolation + +**Implementation:** + +- Single PostgreSQL instance serves all services +- Single Valkey instance serves all services +- Each service creates its own database/user (PostgreSQL) +- Each service gets its own ACL user (Valkey) + +### 2. Multi-Layer Isolation + +#### PostgreSQL Isolation + +Each service: + +- Gets its own database +- Gets its own user with minimal privileges +- Cannot access other services' data + +**Security:** + +- `NOSUPERUSER`: Cannot create other superusers +- `NOCREATEDB`: Cannot create databases +- `NOCREATEROLE`: Cannot create roles +- `priv: ALL` on own database only + +#### Valkey/Redis Isolation + +**Important:** Valkey ACL files do NOT support comments. The `users.acl` file must contain only ACL rules, one per line. All documentation should be in role README files, not in the ACL file itself. +Each service gets its own ACL user with: + +- Unique credentials (username/password) +- Key pattern restrictions (can only access specific key prefixes) +- Command restrictions (deny dangerous commands like FLUSHDB, KEYS, CONFIG) +- Database number assignment (0-15) for additional logical separation + +**Security (ACL-based):** + +- Key patterns: `~immich_bull*` restricts access to matching keys only +- Command groups: `-@dangerous` denies FLUSHDB, FLUSHALL, KEYS, CONFIG, etc. +- Selective grants: `+@read +@write +@pubsub` only allows necessary operations +- Channel access: `&*` for pub/sub (job queues like BullMQ) +- Lua scripting: `+eval +evalsha` only when required (e.g., BullMQ) + +**Defense-in-depth:** + +1. ACL users with restricted permissions (primary) +2. Database number isolation (secondary) +3. Key pattern enforcement (tertiary) + +### 3. Container-to-Host Communication + +**Challenge:** Containers need to reach system PostgreSQL/Valkey + +**Solution:** + +- Rootless Podman with `pasta` networking and `--map-host-loopback={{ podman_gw_gateway }}` +- Inside containers, the host's loopback is reachable at `{{ podman_gw_gateway }}` (default `100.64.0.1`) +- Use `host.containers.internal` (resolves to the same address) or the literal gateway IP +- Avoids insecure `network_mode: host` + +The default is configured in `/etc/containers/containers.conf` via the +`podman` role: + +```ini +[network] +default_rootless_network_cmd = "pasta" +pasta_options = ["--map-host-loopback", "100.64.0.1"] +``` + +Note: `default_rootless_network_cmd` only affects `podman run`. `podman play +kube` defaults to a bridge network, which rootless users cannot create. Pod +services must pass the flag explicitly on the CLI: + +``` +ExecStart=/usr/bin/podman play kube --replace \ + --network=pasta:--map-host-loopback={{ podman_gw_gateway }} myservice.yaml +``` + +### 4. Nginx Reverse Proxy Pattern + +**Why:** Independent deployments, zero-downtime reloads + +**Implementation:** + +- Each service deploys its own vhost config to `{{ nginx_conf_dir }}/.conf` +- Default `nginx_conf_dir: /etc/nginx/conf.d` (configurable in inventory) +- Services control exposure via `_nginx_enabled` variable +- Nginx reloads gracefully when configs change +- **Always use `{{ nginx_conf_dir }}` variable, never hardcode paths** + +### 5. OS Abstraction + +**Why:** Support multiple distributions (Arch Linux, Debian/Ubuntu) + +**Implementation:** + +``` +roles/postgres/vars/ + ├── archlinux.yml # Arch-specific (user: postgres, package: postgresql) + └── debian.yml # Debian-specific (user: postgres, package: postgresql) +``` + +Tasks load: `with_first_found: ["{{ ansible_facts['os_family'] }}.yml", "debian.yml"]` + +## Creating a New Service Role + +### 1. Directory Structure + +``` +roles/myservice/ +├── defaults/main.yml # Variables +├── tasks/main.yml # Main tasks +├── handlers/main.yml # Handlers +├── templates/ +│ ├── myservice.yaml.j2 # Kubernetes Pod spec (if containerized) +│ ├── myservice.service.j2 # systemd user unit (if containerized) +│ └── nginx-vhost.conf.j2 # If web-accessible +├── meta/main.yml # Dependencies +└── README.md # Documentation +``` + +### 2. Meta Dependencies + +```yaml +dependencies: + - role: podman # If using containers + - role: postgres # If needs database + - role: redis # If needs cache +``` + +**Important:** Only include dependencies that are **always** required. Optional dependencies (like nginx for reverse proxy) should be added explicitly in playbooks, not in `meta/main.yml`. + +### 3. Rootless Podman and User Systemd Services + +**Architecture:** This is a single-user administrative server running rootless Podman. All containerized services: + +- Run as `{{ ansible_user }}` (rootless Podman) +- Have files owned by `{{ ansible_user }}` +- Use systemd user services (not system services) +- Require lingering to start at boot without login + +**Critical Implementation Details:** + +1. **User Systemd Services Template:** + +```jinja2 +[Unit] +Description=My Service + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir }}/myservice +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} myservice.yaml +ExecStop=/usr/bin/podman kube down myservice.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target +``` + +**Why `podman kube play --service-container=true`:** All containerized +services in this repo use Kubernetes Pod manifests deployed via +`podman kube play`. The `--service-container=true` flag spawns an extra +long-lived container whose lifetime mirrors the pod, so the systemd unit's +main PID stays alive and emits sd_notify. This is what makes +`Type=notify`, `Restart=on-failure`, and `systemctl --user --failed` work +correctly — without it, `kube play` exits 0 immediately and systemd stays +in `active (exited)` forever, even while containers inside the pod +crash-loop. The explicit `--network=pasta:...` flag is required because +`podman kube play` defaults to a bridge network, which rootless users +cannot create (see section 3). + +**Do not use** `Type=oneshot` + `RemainAfterExit=true` for podman pods. +That pattern silently hides crash loops from systemd. + +**Pod manifest must use `restartPolicy: Never` on every container.** +systemd is the single owner of the restart loop: a container exit takes the +pod down, which takes the service container down, which fails the unit, +which triggers `Restart=on-failure`. With `restartPolicy: Always` or +`OnFailure`, podman retries internally and systemd never sees the failure. + +**Future direction:** see `roadmap/2026-05-29-quadlet.md` for the planned migration to +native Podman Quadlet units (`.kube` files), which removes most of this +boilerplate. + +**Critical differences from system services:** + +- `WantedBy=default.target` (NOT `multi-user.target`) +- No `network-online.target` dependency (doesn't exist in user systemd) +- User services start after the system is up, so network dependencies are implicit + +2. **Service File Placement:** + +```yaml +- name: Get home directory for {{ ansible_user }} + ansible.builtin.getent: + database: passwd + key: "{{ ansible_user }}" + +- name: Set user home directory fact + ansible.builtin.set_fact: + user_home_dir: "{{ getent_passwd[ansible_user][4] }}" + +- name: Create systemd user directory + ansible.builtin.file: + path: "{{ user_home_dir }}/.config/systemd/user" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Deploy systemd service + ansible.builtin.template: + src: myservice.service.j2 + dest: "{{ user_home_dir }}/.config/systemd/user/myservice.service" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Reload systemd user +``` + +**Why getent + set_fact:** `ansible_env.HOME` is evaluated in the controller/initial context and may resolve to `/root` instead of the target user's home. Using `getent` to query `/etc/passwd` provides the correct home directory (field 4), and storing it in a fact makes subsequent references cleaner and more readable. + +3. **Enable Lingering:** + +```yaml +- name: Enable lingering for user {{ ansible_user }} + ansible.builtin.command: "loginctl enable-linger {{ ansible_user }}" + when: ansible_user != 'root' +``` + +Lingering ensures the user's systemd instance starts at boot and persists when the user is not logged in. + +4. **Service Management:** + +```yaml +- name: Enable and start service (user scope) + ansible.builtin.command: "systemctl --user enable --now myservice.service" + become_user: "{{ ansible_user }}" +``` + +**Why `podman play kube`:** Kubernetes Pod manifests are portable and let multiple containers share a network namespace without defining a Compose network. The `--network=pasta:...` CLI flag overrides the default bridge (which rootless users cannot create) and inherits the same loopback mapping configured in `containers.conf`. + +### 4. Password Validation Pattern (REQUIRED) + +All roles requiring passwords **must** validate them at the start of tasks: + +```yaml +- name: Validate required passwords are set + ansible.builtin.assert: + that: + - myservice_password is defined + - myservice_password | length >= 12 + fail_msg: | + myservice_password is required (min 12 chars). + See roles/myservice/defaults/main.yml for configuration instructions. + success_msg: "Password validation passed" +``` + +**Why this pattern:** + +- Prevents accidental deployment with missing/weak passwords +- Fails fast with clear error message +- Directs users to defaults/main.yml for setup instructions +- Keeps error messages concise (target audience: minimal tech knowledge) + +**In defaults/main.yml, passwords should be undefined:** + +```yaml +# myservice_password: "" # Intentionally undefined - role will fail if not set +``` + +**Never use "changeme" defaults** - always fail if password not explicitly set. + +## Best Practices + +### Ansible Usage + +**`become: true` is redundant** - All playbooks are run with `--ask-become-pass`, so every task already runs with elevated privileges. Only use `become_user` to switch to a specific user (e.g., `become_user: postgres`). + +### Security + +1. **Passwords**: Always use Ansible Vault in production + + ```bash + ansible-vault encrypt_string 'password' --name 'myservice_db_password' + ``` + +2. **Bind addresses**: PostgreSQL and Redis bind to `127.0.0.1` only + +3. **Database users**: Minimal privileges (NOSUPERUSER, NOCREATEDB, NOCREATEROLE) + +4. **Nginx**: Only expose services that need external access + +### Variable Naming + +- Role-specific: `_variable_name` +- Generic (cross-role): Add to `.ansible-lint` skip list + +### File Permissions + +- Config files: `0644` +- Secrets: `0640` or `0600` +- Directories: `0755` +- Data directories: `0750` + +### Idempotency + +- Use `creates:` with command/shell +- Use `changed_when: false` for read-only operations +- Use appropriate `when:` conditions + +### Handlers + +- Use `notify` instead of direct state changes +- Keep in `handlers/main.yml` +- Common: `Reload nginx`, `Restart PostgreSQL`, `Reload systemd` diff --git a/roles/fdroid/templates/fdroid.service.j2 b/roles/fdroid/templates/fdroid.service.j2 new file mode 100644 index 0000000..48a1eec --- /dev/null +++ b/roles/fdroid/templates/fdroid.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=F-Droid Repository Server + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/fdroid +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} fdroid.yaml +ExecStop=/usr/bin/podman kube down fdroid.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/fdroid/templates/fdroid.yaml.j2 b/roles/fdroid/templates/fdroid.yaml.j2 new file mode 100644 index 0000000..c246fee --- /dev/null +++ b/roles/fdroid/templates/fdroid.yaml.j2 @@ -0,0 +1,53 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: fdroid + labels: + app: fdroid +spec: + containers: + - name: server + image: {{ fdroid_image }}:{{ fdroid_version }} + ports: + - containerPort: 80 + hostPort: {{ fdroid_port }} + env: + - name: TZ + value: "Europe/Paris" + - name: FDROID_REPO_URL + value: "{{ fdroid_repo_url }}" + - name: FDROID_REPO_NAME + value: "{{ fdroid_repo_name }}" + - name: FDROID_REPO_DESCRIPTION + value: "{{ fdroid_repo_description }}" + - name: FDROID_REPO_ICON + value: "{{ fdroid_repo_icon }}" + - name: FDROID_UPDATE_INTERVAL + value: "{{ fdroid_update_interval }}" + command: ["bash", "-c"] + args: ["apache2ctl -D FOREGROUND & fdroid update -c && while true; do sleep {{ fdroid_update_interval }} && fdroid update; done"] + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: fdroid-data + mountPath: /fdroid + - name: fdroid-repo + mountPath: /var/www/html/repo + readOnly: true + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: fdroid-data + hostPath: + path: {{ fdroid_data_dir }} + type: Directory + - name: fdroid-repo + hostPath: + path: {{ fdroid_data_dir }}/repo + type: Directory diff --git a/roles/gitea/templates/gitea.service.j2 b/roles/gitea/templates/gitea.service.j2 index b1e0d8e..5123e05 100644 --- a/roles/gitea/templates/gitea.service.j2 +++ b/roles/gitea/templates/gitea.service.j2 @@ -2,13 +2,14 @@ Description=Gitea Git Service [Service] -Type=oneshot -RemainAfterExit=true +Type=notify +NotifyAccess=all WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/gitea -ExecStart=/usr/bin/podman play kube --replace --network=pasta:--map-host-loopback={{ podman_gw_gateway }} gitea.yaml -ExecStop=/usr/bin/podman play kube --down gitea.yaml +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} gitea.yaml +ExecStop=/usr/bin/podman kube down gitea.yaml Restart=on-failure RestartSec=10 +TimeoutStartSec=180 [Install] WantedBy=default.target diff --git a/roles/gitea/templates/gitea.yaml.j2 b/roles/gitea/templates/gitea.yaml.j2 index df1bafd..2848e59 100644 --- a/roles/gitea/templates/gitea.yaml.j2 +++ b/roles/gitea/templates/gitea.yaml.j2 @@ -41,7 +41,7 @@ spec: readOnly: true - name: gitea-data mountPath: /data - restartPolicy: Always + restartPolicy: Never volumes: - name: localtime diff --git a/roles/immich/templates/immich.service.j2 b/roles/immich/templates/immich.service.j2 index 5c4934b..55abb84 100644 --- a/roles/immich/templates/immich.service.j2 +++ b/roles/immich/templates/immich.service.j2 @@ -2,13 +2,14 @@ Description=Immich Media Server [Service] -Type=oneshot -RemainAfterExit=true +Type=notify +NotifyAccess=all WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/immich -ExecStart=/usr/bin/podman play kube --replace --network=pasta:--map-host-loopback={{ podman_gw_gateway }} immich.yaml -ExecStop=/usr/bin/podman play kube --down immich.yaml +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} immich.yaml +ExecStop=/usr/bin/podman kube down immich.yaml Restart=on-failure RestartSec=10 +TimeoutStartSec=180 [Install] WantedBy=default.target diff --git a/roles/immich/templates/immich.yaml.j2 b/roles/immich/templates/immich.yaml.j2 index 8559ee4..4065970 100644 --- a/roles/immich/templates/immich.yaml.j2 +++ b/roles/immich/templates/immich.yaml.j2 @@ -53,7 +53,7 @@ spec: periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 - restartPolicy: Always + restartPolicy: Never - name: machine-learning image: {{ immich_ml_image }}:{{ immich_version }} @@ -72,7 +72,7 @@ spec: periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 - restartPolicy: Always + restartPolicy: Never volumes: - name: localtime diff --git a/roles/matrix/templates/matrix.service.j2 b/roles/matrix/templates/matrix.service.j2 new file mode 100644 index 0000000..b34dd2a --- /dev/null +++ b/roles/matrix/templates/matrix.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=Matrix Synapse + Element Web +Wants=network-online.target +After=network-online.target + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/matrix +ExecStart=/usr/bin/podman kube play --replace --service-container=true matrix.yaml +ExecStop=/usr/bin/podman kube down matrix.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/matrix/templates/matrix.yaml.j2 b/roles/matrix/templates/matrix.yaml.j2 new file mode 100644 index 0000000..88a6e04 --- /dev/null +++ b/roles/matrix/templates/matrix.yaml.j2 @@ -0,0 +1,63 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: matrix + labels: + app: matrix +spec: + containers: + - name: synapse + image: {{ synapse_image }}:{{ synapse_version }} + ports: + - containerPort: 8008 + hostPort: {{ synapse_port }} +{% if synapse_enable_federation %} + - containerPort: 8448 + hostPort: {{ synapse_federation_port }} +{% endif %} + env: + - name: SYNAPSE_CONFIG_PATH + value: /data/homeserver.yaml + - name: TZ + value: "{{ matrix_timezone }}" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: synapse-data + mountPath: /data + livenessProbe: + httpGet: + path: /health + port: 8008 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + restartPolicy: Never + + - name: element + image: {{ element_image }}:{{ element_version }} + ports: + - containerPort: 80 + hostPort: 8080 + volumeMounts: + - name: element-config + mountPath: /app/config.json + subPath: config.json + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: synapse-data + hostPath: + path: {{ synapse_data_dir }} + type: Directory + - name: element-config + hostPath: + path: {{ element_data_dir }} + type: Directory diff --git a/roles/ntfy/templates/ntfy.service.j2 b/roles/ntfy/templates/ntfy.service.j2 index a45c957..9335750 100644 --- a/roles/ntfy/templates/ntfy.service.j2 +++ b/roles/ntfy/templates/ntfy.service.j2 @@ -2,13 +2,14 @@ Description=Ntfy Notification Service [Service] -Type=oneshot -RemainAfterExit=true +Type=notify +NotifyAccess=all WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/ntfy -ExecStart=/usr/bin/podman play kube --replace --network=pasta:--map-host-loopback={{ podman_gw_gateway }} ntfy.yaml -ExecStop=/usr/bin/podman play kube --down ntfy.yaml +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} ntfy.yaml +ExecStop=/usr/bin/podman kube down ntfy.yaml Restart=on-failure RestartSec=10 +TimeoutStartSec=180 [Install] WantedBy=default.target diff --git a/roles/ntfy/templates/ntfy.yaml.j2 b/roles/ntfy/templates/ntfy.yaml.j2 index 268c9fe..b0920ad 100644 --- a/roles/ntfy/templates/ntfy.yaml.j2 +++ b/roles/ntfy/templates/ntfy.yaml.j2 @@ -36,7 +36,7 @@ spec: periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 - restartPolicy: Always + restartPolicy: Never volumes: - name: localtime diff --git a/roles/qfieldcloud/templates/qfieldcloud.service.j2 b/roles/qfieldcloud/templates/qfieldcloud.service.j2 new file mode 100644 index 0000000..a877829 --- /dev/null +++ b/roles/qfieldcloud/templates/qfieldcloud.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=QFieldCloud Application + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/qfieldcloud +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network {{ qfieldcloud_podman_network }} qfieldcloud.yaml +ExecStop=/usr/bin/podman kube down qfieldcloud.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/qfieldcloud/templates/qfieldcloud.yaml.j2 b/roles/qfieldcloud/templates/qfieldcloud.yaml.j2 new file mode 100644 index 0000000..3564c7a --- /dev/null +++ b/roles/qfieldcloud/templates/qfieldcloud.yaml.j2 @@ -0,0 +1,126 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: qfieldcloud + labels: + app: qfieldcloud +spec: + containers: + - name: app + image: {{ qfieldcloud_app_image }}:{{ qfieldcloud_version }} + command: + - gunicorn + - qfieldcloud.wsgi:application + - --bind + - 0.0.0.0:8000 + - --timeout + - "{{ qfieldcloud_gunicorn_timeout }}" + - --max-requests + - "{{ qfieldcloud_gunicorn_max_requests }}" + - --workers + - "{{ qfieldcloud_gunicorn_workers }}" + - --threads + - "{{ qfieldcloud_gunicorn_threads }}" + ports: + - containerPort: 8000 + hostPort: {{ qfieldcloud_app_port }} + env: + - name: DJANGO_ALLOWED_HOSTS + value: "{{ qfieldcloud_hostname }} localhost 127.0.0.1 app" + - name: DJANGO_SETTINGS_MODULE + value: qfieldcloud.settings + - name: SECRET_KEY + value: "{{ qfieldcloud_secret_key }}" + - name: SALT_KEY + value: "{{ qfieldcloud_salt_key }}" + - name: DEBUG + value: "{{ qfieldcloud_debug }}" + - name: ENVIRONMENT + value: "{{ qfieldcloud_environment }}" + - name: POSTGRES_DB + value: "{{ qfieldcloud_postgres_db_name }}" + - name: POSTGRES_USER + value: "{{ qfieldcloud_postgres_user }}" + - name: POSTGRES_PASSWORD + value: "{{ qfieldcloud_postgres_password }}" + - name: POSTGRES_HOST + value: "{{ qfieldcloud_postgres_host }}" + - name: POSTGRES_PORT + value: "{{ qfieldcloud_postgres_port }}" + - name: POSTGRES_SSLMODE + value: "{{ qfieldcloud_postgres_sslmode }}" + - name: STORAGES + value: '{"default":{"BACKEND":"qfieldcloud.filestorage.backend.QfcS3Boto3Storage","OPTIONS":{"access_key":"{{ qfieldcloud_s3_access_key }}","secret_key":"{{ qfieldcloud_s3_secret_key }}","bucket_name":"{{ qfieldcloud_s3_bucket }}","region_name":"{{ qfieldcloud_s3_region }}","endpoint_url":"{{ qfieldcloud_s3_endpoint_url }}"},"QFC_IS_LEGACY":false}}' + - name: QFIELDCLOUD_HOST + value: "{{ qfieldcloud_hostname }}" + - name: QFIELDCLOUD_ADMIN_URI + value: "{{ qfieldcloud_admin_uri }}" + - name: QFIELDCLOUD_SUBSCRIPTION_MODEL + value: "{{ qfieldcloud_subscription_model }}" + - name: QFIELDCLOUD_ACCOUNT_ADAPTER + value: "{{ qfieldcloud_account_adapter }}" + - name: QFIELDCLOUD_PASSWORD_LOGIN_IS_ENABLED + value: "{{ qfieldcloud_password_login_enabled }}" + - name: QFIELDCLOUD_AUTH_TOKEN_EXPIRATION_HOURS + value: "{{ qfieldcloud_auth_token_expiration_hours }}" + - name: QFIELDCLOUD_USE_I18N + value: "{{ qfieldcloud_use_i18n }}" + - name: QFIELDCLOUD_DEFAULT_LANGUAGE + value: "{{ qfieldcloud_default_language }}" + - name: QFIELDCLOUD_DEFAULT_TIME_ZONE + value: "{{ qfieldcloud_default_timezone }}" + - name: QFIELDCLOUD_WORKER_QFIELDCLOUD_URL + value: http://localhost:8000/api/v1/ + - name: QFIELDCLOUD_QGIS_IMAGE_NAME + value: "{{ qfieldcloud_qgis_image }}:{{ qfieldcloud_version }}" + - name: QFIELDCLOUD_DEFAULT_NETWORK + value: {{ qfieldcloud_podman_network }} + - name: ACCOUNT_EMAIL_VERIFICATION + value: "{{ qfieldcloud_account_email_verification }}" + - name: SOCIALACCOUNT_PROVIDERS + value: "{{ qfieldcloud_socialaccount_providers }}" + - name: EMAIL_HOST + value: "{{ qfieldcloud_email_host }}" + - name: EMAIL_PORT + value: "{{ qfieldcloud_email_port }}" + - name: EMAIL_USE_TLS + value: "{{ qfieldcloud_email_use_tls }}" + - name: EMAIL_USE_SSL + value: "{{ qfieldcloud_email_use_ssl }}" + - name: EMAIL_HOST_USER + value: "{{ qfieldcloud_email_host_user }}" + - name: EMAIL_HOST_PASSWORD + value: "{{ qfieldcloud_email_host_password }}" + - name: DEFAULT_FROM_EMAIL + value: "{{ qfieldcloud_email_from }}" + - name: TMP_DIRECTORY + value: /tmp + - name: SENTRY_DSN + value: "{{ qfieldcloud_sentry_dsn }}" + - name: SENTRY_SAMPLE_RATE + value: "{{ qfieldcloud_sentry_sample_rate }}" + - name: SENTRY_RELEASE + value: "{{ qfieldcloud_sentry_release }}" + - name: SENTRY_ENVIRONMENT + value: "{{ qfieldcloud_environment }}" + volumeMounts: + - name: staticfiles + mountPath: /usr/src/app/staticfiles + - name: mediafiles + mountPath: /usr/src/app/mediafiles + restartPolicy: Never + + - name: memcached + image: docker.io/library/memcached:1 + restartPolicy: Never + + volumes: + - name: staticfiles + hostPath: + path: {{ podman_projects_dir | default('/opt/podman') }}/qfieldcloud/staticfiles + type: Directory + - name: mediafiles + hostPath: + path: {{ podman_projects_dir | default('/opt/podman') }}/qfieldcloud/mediafiles + type: Directory diff --git a/roles/unifi/templates/unifi.service.j2 b/roles/unifi/templates/unifi.service.j2 new file mode 100644 index 0000000..5cebb14 --- /dev/null +++ b/roles/unifi/templates/unifi.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=UniFi Network Controller + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/unifi +ExecStart=/usr/bin/podman kube play --replace --service-container=true unifi.yaml +ExecStop=/usr/bin/podman kube down unifi.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/unifi/templates/unifi.yaml.j2 b/roles/unifi/templates/unifi.yaml.j2 new file mode 100644 index 0000000..1f62281 --- /dev/null +++ b/roles/unifi/templates/unifi.yaml.j2 @@ -0,0 +1,38 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: unifi + labels: + app: unifi +spec: + hostNetwork: true + containers: + - name: controller + image: {{ unifi_image }}:{{ unifi_version }} + env: + - name: TZ + value: "{{ unifi_timezone }}" + - name: SYSTEM_IP + value: "{{ unifi_bind_address }}" + - name: JVM_MAX_HEAP_SIZE + value: "{{ unifi_jvm_max_heap_size }}" + - name: UNIFI_STDOUT + value: "true" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: unifi-data + mountPath: /unifi + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: unifi-data + hostPath: + path: {{ unifi_data_dir }} + type: Directory diff --git a/roles/uptime_kuma/templates/uptime-kuma.service.j2 b/roles/uptime_kuma/templates/uptime-kuma.service.j2 index a69d58a..99c0b67 100644 --- a/roles/uptime_kuma/templates/uptime-kuma.service.j2 +++ b/roles/uptime_kuma/templates/uptime-kuma.service.j2 @@ -2,13 +2,14 @@ Description=Uptime Kuma Monitoring Service [Service] -Type=oneshot -RemainAfterExit=true +Type=notify +NotifyAccess=all WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/uptime-kuma -ExecStart=/usr/bin/podman play kube --replace uptime-kuma.yaml -ExecStop=/usr/bin/podman play kube --down uptime-kuma.yaml +ExecStart=/usr/bin/podman kube play --replace --service-container=true uptime-kuma.yaml +ExecStop=/usr/bin/podman kube down uptime-kuma.yaml Restart=on-failure RestartSec=10 +TimeoutStartSec=180 [Install] WantedBy=default.target diff --git a/roles/uptime_kuma/templates/uptime-kuma.yaml.j2 b/roles/uptime_kuma/templates/uptime-kuma.yaml.j2 index 191bb7b..4bf40fc 100644 --- a/roles/uptime_kuma/templates/uptime-kuma.yaml.j2 +++ b/roles/uptime_kuma/templates/uptime-kuma.yaml.j2 @@ -21,7 +21,7 @@ spec: readOnly: true - name: uptime-kuma-data mountPath: /app/data - restartPolicy: Always + restartPolicy: Never volumes: - name: localtime