| name | home-server-role-creator |
| description | 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. |
| allowed-tools | Read, Glob, Grep, Write, Edit, Bash |
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:
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:
---
# 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:
---
# 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 creationinstall.yml- Service installation (method varies by type)service.yml- Systemd service managementselinux.yml- SELinux contexts and ports
Conditional:
configure.yml- If service needs configuration filesrepository.yml- If package needs external repositoryquadlet.yml- If using Podman containers
For detailed implementation examples, see references/role-examples.md.
Step 3.4: Create handlers/main.yml
Standard Services:
---
# 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:
---
# 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
---
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)
---
# [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
---
# 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:
- 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:
Add port variable to
roles/nginx/defaults/main.yml:nginx_service_port: 8080Create
roles/nginx/templates/conf.d/[service].conf.j2:# 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)
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)
client_max_body_size 50G; client_body_timeout 600s; proxy_read_timeout 600s; proxy_buffering off; proxy_request_buffering off;
- Add template to
roles/nginx/tasks/configure.ymlloop
Step 4.3: SELinux Integration
Standard SELinux configuration in tasks/selinux.yml:
---
# 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- Executablesvar_lib_t- Service directoriespublic_content_rw_t- Writable contentcontainer_file_t- Container volumeshttp_port_t- HTTP ports
Step 4.4: DNS Rewrite Integration
Add to inventory/host_vars/ndelucca-server.yml:
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:
---
# [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:
- role: [service_name]
tags: ['service', 'category']
6. Testing and Deployment
Step 6.1: Syntax Check
ansible-playbook playbooks/[service].yml --syntax-check -l ndelucca-server
Step 6.2: Deploy
CRITICAL: Always use ansible-host-limiter skill when running playbooks!
ansible-playbook playbooks/[service].yml -l ndelucca-server
Step 6.3: Verification
Use references/checklists.md for comprehensive post-deployment verification checklist.
Essential checks:
# 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:
- Download archive from GitHub/URL
- Extract to temporary directory
- Copy binary to
/usr/local/bin - Create systemd unit file
- Deploy configuration file
See: references/role-examples.md - FileBrowser example
Package Installation (Jellyfin, Cockpit)
Key tasks:
- Add external repository (if needed)
- Install via DNF
- Use system-managed systemd service
- Configure via files or web UI
See: references/role-examples.md - Jellyfin example
Container Installation (Immich)
Key tasks:
- Install Podman (>= 4.4)
- Enable user lingering
- Create Kubernetes YAML pod definition
- Deploy Quadlet .kube unit
- Manage as systemd user service
See: references/role-examples.md - Immich example
Mandatory Rules and Conventions
Critical Rules
Always use ansible-host-limiter skill - Every ansible-playbook command MUST include
-l ndelucca-serverService locality - All web services MUST bind to
127.0.0.1, never0.0.0.0NGINX as gateway - All web services MUST be accessed through NGINX reverse proxy
Firewall orchestration - Firewall rules live in central
roles/firewall/, not in service rolesSELinux is mandatory - Always configure SELinux contexts and ports
User consistency - Default to
ndeluccauser for all servicesRootless 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]/dataor 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:
---
- 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):
Define variable in
defaults/main.yml:service_data_location: "{{ service_base_dir }}/data"Override in
host_vars/ndelucca-server.yml:service_data_location: /srv/disks/D-Draco/media/ServiceApply SELinux context in
tasks/selinux.yml:- 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):
---
# 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
- Service won't start → Check SELinux denials:
ausearch -m avc - Not accessible via NGINX → Check SELinux boolean:
httpd_can_network_connect - Port conflicts → Verify port not already in use:
ss -tlnp - Permission denied → Check file ownership and SELinux contexts
Essential Commands
# 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:
- Plan: Choose installation method, identify integrations
- Create: Role structure with required files
- Implement: Follow patterns for chosen installation method
- Integrate: Firewall, NGINX, SELinux, DNS
- Test: Syntax check, deploy with
-l ndelucca-server, verify - 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.