Host: Rocky Linux 9 (RHEL-family)
User: appuser (UID 1000)
Rootless containers (ως appuser):
├── nginx → port 8080 (NAT από 80 με firewalld)
├── myapp → internal network
└── postgres → internal network, named volume
systemd (user scope):
├── nginx.service ← quadlet
├── myapp.service ← quadlet
└── postgres.service ← quadlet
Auto-start: loginctl enable-linger appuser
---
# rootless-stack.yml
- name: Rootless Podman Stack
hosts: "{{ target | default('nextcloud') }}"
become: true
gather_facts: true
vars:
podman_user: appuser
podman_uid: 1000
app_version: "{{ new_version | mandatory }}"
app_domain: "example.com"
quadlet_dir: "/home/{{ podman_user }}/.config/containers/systemd"
secrets_dir: "/home/{{ podman_user }}/.secrets"
tasks:
# ════════════════════════════════════════
# PHASE 1: PREREQUISITES
# ════════════════════════════════════════
- name: "📦 Install Podman stack"
ansible.builtin.package:
name: [podman, podman-compose, buildah, slirp4netns, fuse-overlayfs]
state: present
- name: "👤 Create appuser"
ansible.builtin.user:
name: "{{ podman_user }}"
uid: "{{ podman_uid }}"
shell: /bin/bash
state: present
- name: "🔑 Linger (auto-start on boot)"
ansible.builtin.command:
cmd: "loginctl enable-linger {{ podman_user }}"
changed_when: true
- name: "📁 Directories"
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ podman_user }}"
mode: '0700'
loop:
- "{{ quadlet_dir }}"
- "{{ secrets_dir }}"
# ════════════════════════════════════════
# PHASE 2: SECRETS (files, not env vars)
# ════════════════════════════════════════
- name: "🔐 Write secrets"
ansible.builtin.copy:
content: "{{ item.value }}"
dest: "{{ secrets_dir }}/{{ item.key }}"
owner: "{{ podman_user }}"
mode: '0600'
loop:
- { key: pg_password, value: "{{ vault_postgres_pass }}" }
- { key: app_secret, value: "{{ vault_app_secret }}" }
loop_control:
label: "{{ item.key }}"
no_log: true
# ════════════════════════════════════════
# PHASE 3: QUADLETS
# ════════════════════════════════════════
- name: "⚙️ PostgreSQL quadlet"
ansible.builtin.copy:
dest: "{{ quadlet_dir }}/postgres.container"
owner: "{{ podman_user }}"
mode: '0644'
content: |
[Unit]
Description=PostgreSQL Database
After=network-online.target
[Container]
Image=postgres:15-alpine
Network=myapp.network
Volume=postgres_data.volume:/var/lib/postgresql/data:Z
Environment=POSTGRES_PASSWORD_FILE=/run/secrets/pg_password
Environment=POSTGRES_USER=appuser
Environment=POSTGRES_DB=mydb
Secret={{ secrets_dir }}/pg_password,type=bind,target=/run/secrets/pg_password
ReadOnly=true
Tmpfs=/tmp:size=64m
Tmpfs=/var/run/postgresql:size=16m
[Service]
Restart=always
[Install]
WantedBy=default.target
notify: Reload user systemd
- name: "⚙️ App quadlet"
ansible.builtin.template:
src: templates/myapp.container.j2
dest: "{{ quadlet_dir }}/myapp.container"
owner: "{{ podman_user }}"
mode: '0644'
notify: Reload user systemd
- name: "⚙️ Nginx quadlet"
ansible.builtin.copy:
dest: "{{ quadlet_dir }}/nginx.container"
owner: "{{ podman_user }}"
mode: '0644'
content: |
[Unit]
Description=Nginx Reverse Proxy
After=myapp.service
[Container]
Image=nginx:1.25-alpine
Network=myapp.network
PublishPort=8080:80
Volume=/home/{{ podman_user }}/nginx.conf:/etc/nginx/nginx.conf:ro,Z
ReadOnly=true
Tmpfs=/tmp:size=32m
Tmpfs=/var/cache/nginx:size=128m
Tmpfs=/var/run:size=16m
[Service]
Restart=always
[Install]
WantedBy=default.target
notify: Reload user systemd
- name: "⚙️ Network quadlet"
ansible.builtin.copy:
dest: "{{ quadlet_dir }}/myapp.network"
owner: "{{ podman_user }}"
mode: '0644'
content: |
[Network]
Internal=false
DNS=true
notify: Reload user systemd
- name: "⚙️ Volume quadlet"
ansible.builtin.copy:
dest: "{{ quadlet_dir }}/postgres_data.volume"
owner: "{{ podman_user }}"
mode: '0644'
content: |
[Volume]
Label=app=myapp
notify: Reload user systemd
# ════════════════════════════════════════
# PHASE 4: FIREWALL (NAT 80 → 8080)
# ════════════════════════════════════════
- name: "🔥 Firewalld: Allow HTTP"
ansible.posix.firewalld:
port: "80/tcp"
permanent: true
state: enabled
notify: Reload firewalld
- name: "🔥 Firewalld: Port forward 80→8080"
ansible.posix.firewalld:
rich_rule: >
rule family="ipv4"
forward-port port="80" protocol="tcp" to-port="8080"
permanent: true
state: enabled
notify: Reload firewalld
# ════════════════════════════════════════
# PHASE 5: START SERVICES
# ════════════════════════════════════════
- name: "▶️ Start quadlet services"
ansible.builtin.systemd:
name: "{{ item }}"
state: started
enabled: true
scope: user
loop:
- postgres
- myapp
- nginx
become: true
become_user: "{{ podman_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ podman_uid }}"
# ════════════════════════════════════════
# PHASE 6: VERIFY
# ════════════════════════════════════════
- name: "🔍 HTTP check"
ansible.builtin.uri:
url: "http://localhost:8080"
status_code: 200
retries: 6
delay: 5
until: not http_check.failed
register: http_check
- name: "📊 Stack Report"
ansible.builtin.debug:
msg:
- "╔══════════════════════════════════════╗"
- "║ 🦭 ROOTLESS STACK DEPLOYED! ║"
- "║ Host: {{ inventory_hostname }} ║"
- "║ User: {{ podman_user }} ║"
- "║ App: v{{ app_version }} ║"
- "╠══════════════════════════════════════╣"
- "║ ✅ Rootless (no root daemon) ║"
- "║ ✅ systemd Quadlets (auto-start) ║"
- "║ ✅ SELinux labels (:Z) ║"
- "║ ✅ Secrets as files (not env vars) ║"
- "╚══════════════════════════════════════╝"
handlers:
- name: Reload user systemd
ansible.builtin.command:
cmd: "systemctl --user daemon-reload"
become: true
become_user: "{{ podman_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ podman_uid }}"
- name: Reload firewalld
ansible.builtin.service:
name: firewalld
state: reloaded
Podman με Ansible
│
├── D11.1: Rootless + daemonless (security benefits)
├── D11.2: Install + subuid/subgid + linger
├── D11.3: containers.podman modules (ίδιο API)
├── D11.4: Quadlets → systemd auto-start
├── D11.5: podman-compose + play kube
└── D11.6: Full rootless production stack
User scope, SELinux, file secrets, firewalld NAT
Ideal για: RHEL/Rocky/Fedora + high-security