Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/home-server-role-creator" ~/.claude/skills/majiayu000-claude-skill-registry-home-server-role-creator && rm -rf "$T"
skills/data/home-server-role-creator/SKILL.mdHome 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:
-
- Complete real-world examples:references/role-examples.md- FileBrowser (binary service)
- Jellyfin (package service)
- Immich (container service with Podman Quadlet)
-
- Comprehensive checklists:references/checklists.md- 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:
- Default variables (always created)defaults/
- Task files (always created)tasks/
- Event handlers (always created)handlers/
- Jinja2 templates (if service needs config files or systemd units)templates/
- Role metadata (always created)meta/
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:
- OS verification, directory creationpreflight.yml
- Service installation (method varies by type)install.yml
- Systemd service managementservice.yml
- SELinux contexts and portsselinux.yml
Conditional:
- If service needs configuration filesconfigure.yml
- If package needs external repositoryrepository.yml
- If using Podman containersquadlet.yml
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.ymlnginx_service_port: 8080 -
Create
: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
looproles/nginx/tasks/configure.yml
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:
- Executablesbin_t
- Service directoriesvar_lib_t
- Writable contentpublic_content_rw_t
- Container volumescontainer_file_t
- HTTP portshttp_port_t
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-server -
Service locality - All web services MUST bind to
, never127.0.0.10.0.0.0 -
NGINX as gateway - All web services MUST be accessed through NGINX reverse proxy
-
Firewall orchestration - Firewall rules live in central
, not in service rolesroles/firewall/ -
SELinux is mandatory - Always configure SELinux contexts and ports
-
User consistency - Default to
user for all servicesndelucca -
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:
or/opt/[service]/srv/[service]
Package installations:
- Binary: System-managed
- Data:
or system default/var/lib/[service]
Container installations:
- Config:
/srv/[service]/config - Data:
or custom location/srv/[service]/data - 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.ymlservice_data_location: "{{ service_base_dir }}/data" -
Override in
:host_vars/ndelucca-server.ymlservice_data_location: /srv/disks/D-Draco/media/Service -
Apply 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
, verify-l ndelucca-server - 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.