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.
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:
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
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 configurationservice_user:ndeluccaservice_group:ndelucca# Directory configurationservice_base_dir:/opt/service# or /srv/serviceservice_working_dir:"{{ service_base_dir }}/data"service_config_dir:"{{ service_base_dir }}/config"# Service configurationservice_name:serviceservice_enabled:trueservice_state:started# Network configurationservice_bind_address:127.0.0.1# ALWAYS 127.0.0.1 for web servicesservice_port:8080# Firewall settingsservice_firewall_enabled:false# false if behind NGINXservice_firewall_zone:FedoraServer# SELinux configurationservice_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:Includepreflightchecksansible.builtin.import_tasks:preflight.ymltags: ['service', 'preflight']
-name:Install [Service]
ansible.builtin.import_tasks:install.ymltags: ['service', 'install']
-name:Configure [Service] applicationansible.builtin.import_tasks:configure.ymltags: ['service', 'configure']
when:service_use_config_file|bool-name:Configuresystemdserviceansible.builtin.import_tasks:service.ymltags: ['service', 'systemd']
-name:ConfigureSELinuxansible.builtin.import_tasks:selinux.ymltags: ['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.
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 127.0.0.1, never 0.0.0.0
NGINX as gateway - All web services MUST be accessed through NGINX reverse proxy
Firewall orchestration - Firewall rules live in central roles/firewall/, not in service roles
SELinux is mandatory - Always configure SELinux contexts and ports
User consistency - Default to ndelucca user for all services
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:
----name:Checkifrepositoryisenabledansible.builtin.command:dnfrepolist--enabledregister:repo_listchanged_when:false-name:Installrepositoryansible.builtin.dnf:name:"[repository_rpm_url]"state:presentdisable_gpg_check:truebecome:truewhen:"'repo-name' not in repo_list.stdout"
Pattern: Custom Storage Location
For services using custom storage (e.g., external disk):
-name:SetSELinuxcontextforcustomstoragecommunity.general.sefcontext:target:"{{ service_data_location }}(/.*)?"setype:container_file_t# or public_content_rw_tstate:present
Pattern: Chained Handlers
For dependent services (e.g., AdGuard must start before NGINX):
---# Use 'listen' to chain handlers-name:restartserviceansible.builtin.systemd:name:"{{ service_name }}"state:restartedbecome:truelisten:restartservice-name:waitforserviceansible.builtin.wait_for:host:127.0.0.1port:"{{ service_port }}"listen:restartservice-name:startdependentserviceansible.builtin.systemd:name:dependent-servicestate:startedbecome:truelisten:restartservice
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