feat: initial commit

Creates foundation for home lab environment
- sets system baselines and creates required directories
- adds `common` role for system baselines
- adds `dns_server` role to set up dns for internal routing
- adds `gitea_server` role  to set up git and action runner
This commit is contained in:
Jordan Del Pilar
2026-06-12 13:02:38 -07:00
commit 80b66bdf0c
17 changed files with 541 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Ansible Vault Password File
.vault-pass.txt
+5
View File
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"gitea"
]
}
+99
View File
@@ -0,0 +1,99 @@
# johto-infra
The core Ansible project for provisioning and managing the Del Pilar homelab infrastructure
## Summary
This contains the primary Ansible configuration-as-code to provision and manage my home infrastructure.
Currently this system manages server baselines, and container deployment for internal DNS (via AdguardHome) and git/ci (via Gitea) using Podman Quadlets
## Scope and Limitations
**Warning** This project is heavily opinionated and designed to work with one specific home lab architecture.
The roles, tasks and templates within this repo are best used as a reference for managing services running as podman quadlets.
If you want to run this repo against your own home lab you will need to rewrite 'inventory.yaml' and all group and host vars to match your specific environment.
## Repository Structure
Below is the directory map for this repo.
```text
├── ansible.cfg
├── group_vars
│ └── all.yaml
├── host_vars
│ ├── goldenrod.yaml
│ └── new-bark.yaml
├── inventory.yaml
├── README.md
├── roles
│ ├── common
│ │ └── tasks
│ │ └── main.yaml
│ ├── dns_server
│ │ ├── handlers
│ │ │ └── main.yaml
│ │ ├── tasks
│ │ │ └── main.yaml
│ │ └── templates
│ │ └── adguard_quadlet.j2
│ └── gitea_server
│ ├── handlers
│ │ └── main.yaml
│ ├── tasks
│ │ └── main.yaml
│ └── templates
│ ├── act_runner_quadlet.j2
│ └── gitea_quadlet.j2
└── site.yaml
````
## Prerequisites
To run this project locally the following tools are required.
All install commands assume you are running Ubuntu/Debian.
- Ansible installed on the control machine
```bash
sudo apt install ansible
```
- Ansible Lint (optional but highly recommended if making changes)
```bash
sudo apt install ansible-lint
```
- SSH access to target nodes
## How to Run
The ultimate goal of this project is a fully automated gitops workflow.
However, until that is fully running below are the instructions to run this project locally.
- Clone the repo
```bash
git clone [email protected]:jdelpilar/johto-infra.git
cd johto-infra
```
- To run all plays in site.yaml and fully initialize the environment run the following command. This will target all nodes.
```bash
ansible-playbook site.yaml
```
- To limit the execution to a single host use the limit flag (-l, --limit)
```bash
# This will only target new-bark
ansible-playbook site.yaml -l new-bark
```
- To limit the execution to only a specific service or tag, use the tag flag (-t, --tags)
```bash
# This will only run dns tasks, but will target all nodes
ansible-playbook site.yaml -t dns
```
- These flags can be combined if needed
```bash
# This will only run dns tasks and only target new-bark
ansible-playbook site.yaml -t dns -l new-bark
```
## Services Deployed
Below is a list of all services currently deployed by this project. This list will be updated as new services are added
Unless otherwise stated all services are run via rootless podman quadlets.
- Internal DNS (dns_server) - AdguardHome for network wide adblocking and local DNS resoultion
- Git Server and CI/CD runner (gitea_server) - Gitea alongside act runner for local git with repo mirroring and local private ci/cd runners
+21
View File
@@ -0,0 +1,21 @@
[defaults]
inventory = ./inventory.yaml
host_key_checking = False
vault_password_file = .vault-pass.txt
forks = 5
stdout_callback = yaml
callbacks_enabled = ansible.posix.profile_tasks
interpreter_python = auto_silent
retry_files_enabled = False
ansible_managed = Ansible managed: modified on %Y-%m-%d %H:%M:%S
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
fact_caching_timeout = 7200
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
[privilege_escalation]
become_method = sudo
+2
View File
@@ -0,0 +1,2 @@
---
github_key_url: https://github.com/redjordan2539.keys
+11
View File
@@ -0,0 +1,11 @@
---
ansible_user: jdelpilar
ansible_become_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
38343539383461386463663730376139636633656434396264626532313465393762323835393931
3030333739373033396639643539613235646364656331660a346431323433366239653535396566
62353361393734623632316463393931636566613965306262313038616665323466373363653665
6434333764636333370a653836343436643631643833333838643264326663333062646235656164
31653163653066653561393333343263653732333335633566363136353164666330
podman_config_base_dir: /home/{{ ansible_user }}/podman
podman_quadlet_base_dir: "/home/{{ ansible_user }}/.config/containers/systemd"
+45
View File
@@ -0,0 +1,45 @@
---
ansible_user: jdelpilar
ansible_become_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66633539643530363735343838646634366161346239366132643762323465323766353230343836
3935666665383830383163363938303239656436333061370a323236303436313836616233333639
38333035643338643439393335356436356434343135663464353863343134386633346466333838
3264376339343239650a383939393663346231386461653738656330336338656436373866306137
3961
podman_config_base_dir: /appdata/podman
podman_quadlet_base_dir: "/home/{{ ansible_user }}/.config/containers/systemd"
dns_services:
- name: adguard
image: docker.io/adguard/adguardhome:latest
category: management
volumes:
- /appdata/podman/adguard/work:/opt/adguardhome/work
- /appdata/podman/adguard/conf:/opt/adguardhome/conf
gitea_services:
- name: gitea
image: docker.io/gitea/gitea:latest
container_owner: jdelpilar
subdomain: git
domain: delpilar.net
port: 3000
volumes:
- /appdata/podman/gitea/data:/var/lib/gitea
- /appdata/podman/gitea/config:/etc/gitea
runner_services:
- name: act_runner
image: docker.io/gitea/act_runner:nightly
container_owner: jdelpilar
gitea_url: https://git.delpilar.net
runner_token: !vault |
$ANSIBLE_VAULT;1.1;AES256
35303036303665376330616430373733363066333332323362623237373262383230316431346338
3862636465623436343062316662666531623562353164650a313434633865656634366136643430
37653263636535656561613663363165313332323164313365306433363036333437373333316632
6563356532323964660a626531303962626236363961356639623263373030663462623235393366
36343736663063313933306465626634383863366538343930386335353762663866623535636465
3461386532333061323435363366653633613035386363633162
+31
View File
@@ -0,0 +1,31 @@
---
all:
children:
servers:
hosts:
new-bark:
ansible_host: 100.64.0.2
goldenrod:
ansible_host: 100.64.0.1
traefik_servers:
hosts:
new-bark:
goldenrod:
dns_servers:
hosts:
new-bark:
media_servers:
hosts:
new-bark:
arr_servers:
hosts:
new-bark:
headscale_servers:
hosts:
goldenrod:
vaultwarden_servers:
hosts:
goldenrod:
gitea_servers:
hosts:
new-bark:
+57
View File
@@ -0,0 +1,57 @@
---
- name: Enable systemd Lingering
ansible.builtin.file:
path: "/var/lib/systemd/linger/{{ ansible_user }}"
state: touch
owner: root
group: root
mode: "0644"
become: true
- name: Deploy workstation public keys from GitHub
ansible.posix.authorized_key:
user: "{{ ansible_user }}"
state: present
key: "{{ github_key_url }}"
exclusive: true
- name: Install Podman
ansible.builtin.package:
name: podman
state: present
become: true
- name: Start Podman Timer
ansible.builtin.systemd:
name: podman-auto-update.timer
state: started
enabled: true
scope: user
- name: Start Podman Socket
ansible.builtin.systemd:
name: podman.socket
state: started
enabled: true
scope: user
- name: Create Podman Config Directory
ansible.builtin.file:
path: "{{ podman_config_base_dir }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0755"
- name: Create Podman Quadlet Directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0755"
with_items:
- "{{ podman_quadlet_base_dir }}/infrastructure"
- "{{ podman_quadlet_base_dir }}/management"
- "{{ podman_quadlet_base_dir }}/media"
- "{{ podman_quadlet_base_dir }}/services"
+12
View File
@@ -0,0 +1,12 @@
- name: Reload systemd User Daemon
ansible.builtin.systemd:
daemon_reload: true
scope: user
become_user: "{{ ansible_user }}"
- name: Restart changed DNS Services
ansible.builtin.systemd:
name: "{{ item.item.name }}"
state: restarted
scope: user
loop: "{{ dns_quadlet_results.results | selectattr('changed', 'equalto', true) | list }}"
+36
View File
@@ -0,0 +1,36 @@
- name: Allow Binding Port 53
ansible.posix.sysctl:
name: net.ipv4.ip_unprivileged_port_start
value: "53"
state: present
reload: true
become: true
- name: Create DNS Service Directories
ansible.builtin.file:
path: "{{ item.1.split(':')[0] }}"
state: directory
mode: "0755"
loop: "{{ lookup('ansible.builtin.subelements', dns_services, 'volumes', {'skip_missing': true}) }}"
- name: Create Podman Quadlets
ansible.builtin.template:
src: adguard_quadlet.j2
dest: "{{ podman_quadlet_base_dir }}/management/{{ item.name }}.container"
owner: "{{ ansible_user }}"
mode: "664"
loop: "{{ dns_services }}"
register: dns_quadlet_results
notify:
- Reload systemd User Daemon
- Restart changed DNS Services
- name: Flush Handlers
ansible.builtin.meta: flush_handlers
- name: Start DNS Services
ansible.builtin.systemd:
name: "{{ item.name }}"
state: started
scope: user
loop: "{{ dns_services }}"
@@ -0,0 +1,44 @@
# {{ ansible_managed }}
[Unit]
After=network-online.target
StartLimitBurst=10
StartLimitIntervalSec=120
[Container]
ContainerName=adguard
Image={{ item.image }}
Network=host
AutoUpdate={{ item.auto_update | default('registry') }}
Label=category={{ item.category | default('services') }}
Label=owner={{ container_owner | default('jdelpilar') }}
HealthCmd=nslookup localhost 127.0.0.1 > /dev/null || exit 1
HealthInterval=30s
HealthRetries=3
HealthTimeout=5s
{% for volume in item.volumes | default([]) %}
Volume={{ volume }}
{% endfor %}
Environment=TZ={{ timezone | default('America/Los_Angeles') }}
{% if item.env is defined %}
{% for key, value in item.env.items() | sort %}
Environment={{ key }}={{ value }}
{% endfor %}
{% endif %}
{% if item.exec is defined %}
Exec={{ item.exec }}
{% endif %}
[Service]
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
+19
View File
@@ -0,0 +1,19 @@
- name: Reload systemd User Daemon
ansible.builtin.systemd:
daemon_reload: true
scope: user
become_user: "{{ ansible_user }}"
- name: Restart changed Gitea Services
ansible.builtin.systemd:
name: "{{ item.item.name }}"
state: restarted
scope: user
loop: "{{ gitea_quadlet_results.results | selectattr('changed', 'equalto', true) | list }}"
- name: Restart changed Runner Services
ansible.builtin.systemd:
name: "{{ item.item.name }}"
state: restarted
scope: user
loop: "{{ runner_quadlet_results.results | selectattr('changed', 'equalto', true) | list }}"
+47
View File
@@ -0,0 +1,47 @@
- name: Create Gitea Directories
ansible.builtin.file:
path: "{{ item.1.split(':')[0] }}"
state: directory
mode: "0755"
loop: "{{ lookup('ansible.builtin.subelements', gitea_services, 'volumes', {'skip_missing': true}) }}"
- name: Create Gitea Quadlets
ansible.builtin.template:
src: gitea_quadlet.j2
dest: "{{ podman_quadlet_base_dir }}/management/{{ item.name }}.container"
owner: "{{ ansible_user }}"
mode: "664"
loop: "{{ gitea_services }}"
register: gitea_quadlet_results # <--- Unique name
notify:
- Reload systemd User Daemon
- Restart changed Gitea Services
- name: Create Act Runner Quadlets
ansible.builtin.template:
src: act_runner_quadlet.j2
dest: "{{ podman_quadlet_base_dir }}/management/{{ item.name }}container"
owner: "{{ ansible_user }}"
mode: "664"
loop: "{{ runner_services }}"
register: runner_quadlet_results # <--- Unique name
notify:
- Reload systemd User Daemon
- Restart changed Runner Services
- name: Flush Handlers
ansible.builtin.meta: flush_handlers
- name: Start Gitea Services
ansible.builtin.systemd:
name: "{{ item.name }}"
state: started
scope: user
loop: "{{ gitea_services }}"
- name: Start Runner Services
ansible.builtin.systemd:
name: "{{ item.name }}"
state: started
scope: user
loop: "{{ runner_services }}"
@@ -0,0 +1,33 @@
# {{ ansible_managed }}
[Unit]
After=network-online.target
After=adguard.service
Requires=adguard.service
StartLimitBurst=10
StartLimitIntervalSec=120
[Container]
ContainerName={{ item.name }}
Image={{ item.image }}
Network=management-net
PodmanArgs=--add-host=git.delpilar.net:172.23.0.2
AutoUpdate=registry
Label=category=management
Label=owner={{ container_owner | default('jdelpilar') }}
Volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock
Environment=TZ='America/Los_Angeles'
Environment=GITEA_INSTANCE_URL={{ item.gitea_url }}
Environment=GITEA_RUNNER_REGISTRATION_TOKEN={{ item.runner_token }}
[Service]
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
@@ -0,0 +1,56 @@
# {{ ansible_managed }}
[Unit]
After=network-online.target
After=adguard.service
Requires=adguard.service
StartLimitBurst=10
StartLimitIntervalSec=120
[Container]
ContainerName={{ item.name }}
Image={{ item.image }}
Network=management-net
PublishPort=2222:22
AutoUpdate=registry
Label=category=management
Label=owner={{ container_owner | default('jdelpilar') }}
{% if item.enable_traefik | default(true) %}
Label=traefik.enable=true
Label=traefik.http.routers.{{ item.name }}.rule=Host(`{{ item.subdomain | default(item.name) }}.{{ base_domain | default('delpilar.net') }}`)
Label=traefik.http.routers.{{ item.name }}.entrypoints={{ item.traefik_entrypoint | default('websecure') }}
Label=traefik.http.routers.{{ item.name }}.tls.certresolver={{ item.traefik_resolver | default('cloudflare') }}
Label=traefik.http.services.{{ item.name }}.loadbalancer.server.port={{ item.port | default(80) }}
Label=traefik.docker.network={{ item.network | default('management-net') }}
Label=traefik.http.routers.{{ item.name }}.tls=true
{% endif %}
HealthCmd=curl -f http://localhost:3000/api/healthz
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
HealthStartPeriod=30s
{% for volume in item.volumes | default([]) %}
Volume={{ volume }}
{% endfor %}
Volume=/etc/timezone:/etc/timezone:ro
Volume=/etc/localtime:/etc/localtime:ro
Environment=TZ={{ timezone | default('America/Los_Angeles') }}
{% if item.env is defined %}
{% for key, value in item.env.items() | sort %}
Environment={{ key }}={{ value }}
{% endfor %}
{% endif %}
[Service]
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
+21
View File
@@ -0,0 +1,21 @@
---
- name: Common Setup
hosts: servers
roles:
- common
tags:
- common
- name: DNS Setup
hosts: dns_servers
roles:
- dns_server
tags:
- dns
- name: Gitea Setup
hosts: gitea_servers
roles:
- gitea_server
tags:
- gitea