Agent skill

home-server-role-creator

Complete guide for adding new self-hosted applications to the home-server Ansible infrastructure. Use this skill when the user wants to add a new service, create a new role, or deploy a new self-hosted application. Covers role structure, integration patterns (firewall, NGINX, SELinux, DNS), installation methods (binary, package, container), and testing procedures.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/home-server-role-creator-ndelucca-environment

SKILL.md

Home Server Role Creator

Purpose

This skill provides comprehensive guidance for adding new self-hosted applications to the home-server Ansible infrastructure. It documents all established patterns, conventions, and integration requirements to ensure consistent, secure, and maintainable role implementations.

When to Use This Skill

Activate this skill when:

  • Adding a new self-hosted service to the home server
  • Creating a new Ansible role for a service
  • Deploying a new application that needs web access via NGINX
  • Integrating a new service with firewall, SELinux, or DNS

Reference Files

This skill includes detailed reference files for in-depth information:

  • references/role-examples.md - Complete real-world examples:

    • FileBrowser (binary service)
    • Jellyfin (package service)
    • Immich (container service with Podman Quadlet)
  • references/checklists.md - Comprehensive checklists:

    • Pre-development checklist
    • Role structure checklist
    • Variable definition checklist
    • Task implementation checklist
    • Integration checklists (firewall, NGINX, SELinux, DNS)
    • Pre-deployment checklist
    • Post-deployment verification checklist
    • Troubleshooting guides

Load these reference files when detailed examples or comprehensive checklists are needed.

Role Creation Workflow

Follow this workflow for every new service:

1. Planning Phase

Determine Installation Method:

Is the service containerized?
├─ Yes → Use Podman Quadlet pattern (see references/role-examples.md: Immich)
└─ No → Is it available in DNF/RPM repositories?
    ├─ Yes → Use Package installation (see references/role-examples.md: Jellyfin)
    └─ No → Use Binary download/installation (see references/role-examples.md: FileBrowser)

Identify Required Integrations:

  • Web interface? → Needs NGINX reverse proxy
  • Needs firewall port access? → Firewall configuration
  • Custom storage locations? → SELinux contexts required
  • Subdomain access? → DNS rewrite in AdGuard

2. Directory Setup

Create the role directory structure:

bash
mkdir -p roles/[service_name]/{defaults,tasks,handlers,templates,meta}

Required directories:

  • defaults/ - Default variables (always created)
  • tasks/ - Task files (always created)
  • handlers/ - Event handlers (always created)
  • templates/ - Jinja2 templates (if service needs config files or systemd units)
  • meta/ - Role metadata (always created)

3. Core Implementation

Step 3.1: Create defaults/main.yml

Define all configurable variables following this pattern:

yaml
---
# Default variables for [Service] role

# Service user configuration
service_user: ndelucca
service_group: ndelucca

# Directory configuration
service_base_dir: /opt/service  # or /srv/service
service_working_dir: "{{ service_base_dir }}/data"
service_config_dir: "{{ service_base_dir }}/config"

# Service configuration
service_name: service
service_enabled: true
service_state: started

# Network configuration
service_bind_address: 127.0.0.1  # ALWAYS 127.0.0.1 for web services
service_port: 8080

# Firewall settings
service_firewall_enabled: false  # false if behind NGINX
service_firewall_zone: FedoraServer

# SELinux configuration
service_manage_selinux: true

See references/checklists.md for complete variable definition checklist.

Step 3.2: Create tasks/main.yml

Orchestration file that imports modular task files:

yaml
---
# Main entry point for [Service] role

- name: Include preflight checks
  ansible.builtin.import_tasks: preflight.yml
  tags: ['service', 'preflight']

- name: Install [Service]
  ansible.builtin.import_tasks: install.yml
  tags: ['service', 'install']

- name: Configure [Service] application
  ansible.builtin.import_tasks: configure.yml
  tags: ['service', 'configure']
  when: service_use_config_file | bool

- name: Configure systemd service
  ansible.builtin.import_tasks: service.yml
  tags: ['service', 'systemd']

- name: Configure SELinux
  ansible.builtin.import_tasks: selinux.yml
  tags: ['service', 'selinux']
  when: service_manage_selinux | bool

Step 3.3: Task Files

Create these task files based on service type:

Always Required:

  • preflight.yml - OS verification, directory creation
  • install.yml - Service installation (method varies by type)
  • service.yml - Systemd service management
  • selinux.yml - SELinux contexts and ports

Conditional:

  • configure.yml - If service needs configuration files
  • repository.yml - If package needs external repository
  • quadlet.yml - If using Podman containers

For detailed implementation examples, see references/role-examples.md.

Step 3.4: Create handlers/main.yml

Standard Services:

yaml
---
# Handlers for [Service] role

- name: daemon-reload
  ansible.builtin.systemd:
    daemon_reload: true
  become: true

- name: restart service
  ansible.builtin.systemd:
    name: "{{ service_name }}"
    state: restarted
  become: true

- name: apply selinux context
  ansible.builtin.command: "restorecon -Rv {{ item }}"
  become: true
  loop:
    - "{{ service_install_dir }}/service"
    - "{{ service_working_dir }}"
  changed_when: false

Rootless Podman Services:

yaml
---
# Handlers for rootless Podman service

- name: daemon-reload-user
  ansible.builtin.systemd:
    daemon_reload: true
    scope: user
  become: true
  become_user: "{{ service_user }}"
  environment:
    XDG_RUNTIME_DIR: "/run/user/{{ service_uid }}"

- name: restart service-pod
  ansible.builtin.systemd:
    name: "{{ service_name }}"
    state: restarted
    scope: user
  become: true
  become_user: "{{ service_user }}"
  environment:
    XDG_RUNTIME_DIR: "/run/user/{{ service_uid }}"

Step 3.5: Create meta/main.yml

yaml
---
galaxy_info:
  author: Naza
  description: Install and configure [Service] on Fedora
  license: MIT
  min_ansible_version: '2.13'
  platforms:
    - name: Fedora
      versions:
        - all

dependencies: []

collections:
  - community.general
  - ansible.posix

4. Integration

Step 4.1: Firewall Integration

Create roles/firewall/tasks/[service_name].yml:

Pattern A: Service Behind NGINX (Most Common)

yaml
---
# [Service] is behind NGINX reverse proxy
# Access via [subdomain].ndelucca-server.com on ports 80/443

- name: Remove old direct port from firewall
  ansible.posix.firewalld:
    port: "{{ service_port }}/tcp"
    zone: "{{ service_firewall_zone }}"
    permanent: true
    immediate: true
    state: disabled
  become: true
  notify: reload firewalld
  ignore_errors: true

Pattern B: Service Needs Direct Access

yaml
---
# Firewall configuration for [Service]

- name: Configure firewall ports
  ansible.posix.firewalld:
    port: "{{ service_port }}/tcp"
    zone: "{{ service_firewall_zone }}"
    permanent: true
    immediate: true
    state: enabled
  become: true
  notify: reload firewalld

Add import to roles/firewall/tasks/main.yml:

yaml
- name: Configure firewall for [Service]
  ansible.builtin.import_tasks: service.yml
  when: service_firewall_enabled | default(true)
  tags: ['firewall-service']

Step 4.2: NGINX Reverse Proxy Integration

If service has web interface:

  1. Add port variable to roles/nginx/defaults/main.yml:

    yaml
    nginx_service_port: 8080
    
  2. Create roles/nginx/templates/conf.d/[service].conf.j2:

    nginx
    # HTTP
    server {
        listen 80;
        server_name [subdomain].{{ nginx_domain }};
    
        location / {
            proxy_pass http://127.0.0.1:{{ nginx_service_port }};
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
    # HTTPS
    server {
        listen 443 ssl;
        http2 on;
        server_name [subdomain].{{ nginx_domain }};
    
        ssl_certificate {{ nginx_ssl_certificate }};
        ssl_certificate_key {{ nginx_ssl_certificate_key }};
    
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
        location / {
            proxy_pass http://127.0.0.1:{{ nginx_service_port }};
            # ... same proxy headers as HTTP
        }
    }
    

NGINX Features to Add When Needed:

  • WebSocket support: For real-time features (Jellyfin, Immich)

    nginx
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
  • Large upload support: For file/media services (FileBrowser, Immich)

    nginx
    client_max_body_size 50G;
    client_body_timeout 600s;
    proxy_read_timeout 600s;
    proxy_buffering off;
    proxy_request_buffering off;
    
  1. Add template to roles/nginx/tasks/configure.yml loop

Step 4.3: SELinux Integration

Standard SELinux configuration in tasks/selinux.yml:

yaml
---
# Configure SELinux for [Service]

- name: Check SELinux status
  ansible.builtin.command: getenforce
  register: selinux_status
  changed_when: false

- name: Install SELinux packages
  ansible.builtin.dnf:
    name: policycoreutils-python-utils
    state: present
  become: true
  when: selinux_status.stdout == "Enforcing"

- name: Set SELinux context for binary
  community.general.sefcontext:
    target: "{{ service_install_dir }}/service"
    setype: bin_t
    state: present
  become: true
  when: selinux_status.stdout == "Enforcing"
  notify: apply selinux context

- name: Set SELinux context for directories
  community.general.sefcontext:
    target: "{{ item.path }}(/.*)?"
    setype: "{{ item.type }}"
    state: present
  become: true
  loop:
    - { path: "{{ service_working_dir }}", type: "var_lib_t" }
    - { path: "{{ service_data_dir }}", type: "container_file_t" }  # or public_content_rw_t
  when: selinux_status.stdout == "Enforcing"
  notify: apply selinux context

- name: Allow service to bind to custom port
  community.general.seport:
    ports: "{{ service_port }}"
    proto: tcp
    setype: http_port_t
    state: present
  become: true
  when:
    - selinux_status.stdout == "Enforcing"
    - service_port != 80 and service_port != 443

Common SELinux Types:

  • bin_t - Executables
  • var_lib_t - Service directories
  • public_content_rw_t - Writable content
  • container_file_t - Container volumes
  • http_port_t - HTTP ports

Step 4.4: DNS Rewrite Integration

Add to inventory/host_vars/ndelucca-server.yml:

yaml
adguard_dns_rewrites:
  # ... existing entries ...
  - domain: [subdomain].ndelucca-server.com
    answer: 192.168.10.10
    enabled: true

Subdomain naming: Use short, descriptive names (files, jellyfin, torrent, gallery, cockpit)

5. Playbook Creation

Step 5.1: Create Service Playbook

Create playbooks/[service].yml:

yaml
---
# [Service]-specific playbook
# Usage: ansible-playbook playbooks/[service].yml -l ndelucca-server

- name: Install and configure [Service]
  hosts: homeservers
  gather_facts: true

  roles:
    - [service_name]

Step 5.2: Update Site Playbook

Add role to playbooks/site.yml:

yaml
    - role: [service_name]
      tags: ['service', 'category']

6. Testing and Deployment

Step 6.1: Syntax Check

bash
ansible-playbook playbooks/[service].yml --syntax-check -l ndelucca-server

Step 6.2: Deploy

CRITICAL: Always use ansible-host-limiter skill when running playbooks!

bash
ansible-playbook playbooks/[service].yml -l ndelucca-server

Step 6.3: Verification

Use references/checklists.md for comprehensive post-deployment verification checklist.

Essential checks:

bash
# Service status
ansible ndelucca-server -m ansible.builtin.systemd -a "name=[service]" --become

# Service listening
ansible ndelucca-server -m shell -a "ss -tlnp | grep [port]"

# Test web access (if applicable)
curl http://[subdomain].ndelucca-server.com
curl https://[subdomain].ndelucca-server.com

Installation Method Patterns

Binary Installation (FileBrowser, Cloud Torrent)

Key tasks:

  1. Download archive from GitHub/URL
  2. Extract to temporary directory
  3. Copy binary to /usr/local/bin
  4. Create systemd unit file
  5. Deploy configuration file

See: references/role-examples.md - FileBrowser example

Package Installation (Jellyfin, Cockpit)

Key tasks:

  1. Add external repository (if needed)
  2. Install via DNF
  3. Use system-managed systemd service
  4. Configure via files or web UI

See: references/role-examples.md - Jellyfin example

Container Installation (Immich)

Key tasks:

  1. Install Podman (>= 4.4)
  2. Enable user lingering
  3. Create Kubernetes YAML pod definition
  4. Deploy Quadlet .kube unit
  5. Manage as systemd user service

See: references/role-examples.md - Immich example

Mandatory Rules and Conventions

Critical Rules

  1. Always use ansible-host-limiter skill - Every ansible-playbook command MUST include -l ndelucca-server

  2. Service locality - All web services MUST bind to 127.0.0.1, never 0.0.0.0

  3. NGINX as gateway - All web services MUST be accessed through NGINX reverse proxy

  4. Firewall orchestration - Firewall rules live in central roles/firewall/, not in service roles

  5. SELinux is mandatory - Always configure SELinux contexts and ports

  6. User consistency - Default to ndelucca user for all services

  7. Rootless when possible - Prefer rootless Podman over rootful containers

Variable Naming Convention

All service role variables follow this pattern:

[service]_user              # Service user (default: ndelucca)
[service]_group             # Service group (default: ndelucca)
[service]_port              # Service port
[service]_bind_address      # Bind address (default: 127.0.0.1)
[service]_base_dir          # Base directory (/srv or /opt)
[service]_working_dir       # Working/data directory
[service]_config_dir        # Configuration directory
[service]_service_name      # Systemd service name
[service]_service_enabled   # Enable on boot (default: true)
[service]_service_state     # Service state (default: started)
[service]_firewall_enabled  # Enable firewall (default: false if behind NGINX)
[service]_firewall_zone     # Firewall zone (default: FedoraServer)
[service]_manage_selinux    # Manage SELinux (default: true)

File Naming Convention

roles/[service_name]/                          # Role directory (lowercase, underscores)
playbooks/[service_name].yml                   # Playbook (matches role name)
roles/firewall/tasks/[service_name].yml        # Firewall tasks
roles/nginx/templates/conf.d/[service].conf.j2 # NGINX config (short name)
/etc/systemd/system/[service_name].service     # Systemd unit
[subdomain].ndelucca-server.com                # DNS subdomain (short, descriptive)

Directory Structure Conventions

Binary installations:

  • Binary: /usr/local/bin/[service]
  • Data: /opt/[service] or /srv/[service]

Package installations:

  • Binary: System-managed
  • Data: /var/lib/[service] or system default

Container installations:

  • Config: /srv/[service]/config
  • Data: /srv/[service]/data or custom location
  • Quadlet: /etc/containers/systemd/users/[uid]/

Common Patterns

Pattern: External Repository Required

For services needing external repository (e.g., RPMFusion):

Create tasks/repository.yml:

yaml
---
- name: Check if repository is enabled
  ansible.builtin.command: dnf repolist --enabled
  register: repo_list
  changed_when: false

- name: Install repository
  ansible.builtin.dnf:
    name: "[repository_rpm_url]"
    state: present
    disable_gpg_check: true
  become: true
  when: "'repo-name' not in repo_list.stdout"

Pattern: Custom Storage Location

For services using custom storage (e.g., external disk):

  1. Define variable in defaults/main.yml:

    yaml
    service_data_location: "{{ service_base_dir }}/data"
    
  2. Override in host_vars/ndelucca-server.yml:

    yaml
    service_data_location: /srv/disks/D-Draco/media/Service
    
  3. Apply SELinux context in tasks/selinux.yml:

    yaml
    - name: Set SELinux context for custom storage
      community.general.sefcontext:
        target: "{{ service_data_location }}(/.*)?"
        setype: container_file_t  # or public_content_rw_t
        state: present
    

Pattern: Chained Handlers

For dependent services (e.g., AdGuard must start before NGINX):

yaml
---
# Use 'listen' to chain handlers

- name: restart service
  ansible.builtin.systemd:
    name: "{{ service_name }}"
    state: restarted
  become: true
  listen: restart service

- name: wait for service
  ansible.builtin.wait_for:
    host: 127.0.0.1
    port: "{{ service_port }}"
  listen: restart service

- name: start dependent service
  ansible.builtin.systemd:
    name: dependent-service
    state: started
  become: true
  listen: restart service

Quick Reference

Typical Role Creation Time

  • Binary service: 30-45 minutes
  • Package service: 20-30 minutes
  • Container service: 60-90 minutes

Files Typically Modified

For each new service, expect to create/modify:

  • Role directory: 6-10 files
  • Firewall: 1 file + 1 import line
  • NGINX: 1 template + 1 variable + 1 loop entry
  • DNS: 1 rewrite entry
  • Playbooks: 1 new playbook + 1 site.yml entry

Most Common Issues

  1. Service won't start → Check SELinux denials: ausearch -m avc
  2. Not accessible via NGINX → Check SELinux boolean: httpd_can_network_connect
  3. Port conflicts → Verify port not already in use: ss -tlnp
  4. Permission denied → Check file ownership and SELinux contexts

Essential Commands

bash
# Syntax check
ansible-playbook playbooks/service.yml --syntax-check -l ndelucca-server

# Deploy
ansible-playbook playbooks/service.yml -l ndelucca-server

# Check service
ansible ndelucca-server -m systemd -a "name=service" --become

# Check logs
ansible ndelucca-server -m shell -a "journalctl -u service -n 50" --become

# Check SELinux
ansible ndelucca-server -m shell -a "ausearch -m avc -ts recent" --become

Summary

When adding a new service:

  1. Plan: Choose installation method, identify integrations
  2. Create: Role structure with required files
  3. Implement: Follow patterns for chosen installation method
  4. Integrate: Firewall, NGINX, SELinux, DNS
  5. Test: Syntax check, deploy with -l ndelucca-server, verify
  6. Document: Update references if new patterns emerge

Critical reminders:

  • Always use ansible-host-limiter skill
  • Services bind to 127.0.0.1
  • Configure SELinux for all directories
  • Use reference files for detailed examples and checklists

For detailed examples, see references/role-examples.md. For comprehensive checklists, see references/checklists.md.

Didn't find tool you were looking for?

Be as detailed as possible for better results