🛠️ How-To: Deploy Sovereign Gitea via Automated Ansible Playbook
This guide details how to automate the standalone deployment of a self-hosted Gitea GitOps stack over rootless Podman using Ansible playbooks. The workflow handles OS dependencies, linger enablement, secret vaulting, 5-year Sovereign TLS certificate generation, Quadlet systemd unit deployment, and installation screen bypass automatically.
1. Overview & Architecture
Using Ansible for deployment guarantees idempotent, reproducible infrastructure provisioning.
Key Architecture Specs
- Default Domain / Host IP:
10.17.250.28(or target inventory host) - Database Backend: PostgreSQL 15 (Alpine)
- Application Server: Gitea 1.26.3
- HTTP/HTTPS Port:
3000(Mapped to container port3000over HTTPS) - SSH Port:
2222(Mapped to container port22for Git over SSH) - Security & TLS: TLS certificates generated via
community.cryptoand signed by Sovereign CA. - Automated Bypass: Bypasses web installation screen programmatically (
GITEA__security__INSTALL_LOCK: "true").
2. Prerequisites & Ansible Setup
Ensure Ansible and required collection packages are installed on the control node:
# Install required Ansible collections for Podman and Crypto tasks
ansible-galaxy collection install containers.podman community.crypto community.general
Verify target inventory connectivity:
ansible -i inventory/hosts.yml target_hosts -m ping
3. Ansible Role & Playbook Structure
The deployment relies on Ansible role variables and task definitions:
Role Defaults (roles/gitea/defaults/main.yml)
gitea_domain: "10.17.250.28"
gitea_root_url: "https://10.17.250.28:3000/"
gitea_http_port: 3000
gitea_ssh_port: 2222
gitea_db_password: "{{ vault_gitea_db_password }}"
gitea_postgres_max_connections: 200
gitea_postgres_shared_buffers: "256MB"
gitea_postgres_effective_cache_size: "1GB"
gitea_postgres_maintenance_work_mem: "64MB"
gitea_postgres_work_mem: "16MB"
Main Task Execution Flow (roles/gitea/tasks/main.yml)
- name: Generate and deploy TLS certificates
ansible.builtin.import_tasks: tls.yml
- name: Ensure Quadlet directory exists
ansible.builtin.file:
path: ~/.config/containers/systemd/
state: directory
mode: '0755'
- name: Deploy Gitea Kube Quadlet YAML (mode 0600 for secrets)
ansible.builtin.template:
src: "gitea-stack.yaml.j2"
dest: "~/.config/containers/systemd/gitea-stack.yaml"
mode: '0600'
- name: Deploy Gitea Kube Quadlet Unit
ansible.builtin.template:
src: "gitea-stack.kube.j2"
dest: "~/.config/containers/systemd/gitea-stack.kube"
mode: '0644'
notify: reload systemd user daemon
- name: Reload systemd user daemon for Quadlet generation
ansible.builtin.shell: systemctl --user daemon-reload
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_user_uid }}"
changed_when: false
- name: Ensure Gitea service is enabled and started
ansible.builtin.systemd:
name: gitea-stack.service
state: started
enabled: yes
scope: user
daemon_reload: yes
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_user_uid }}"
Sovereign TLS Task Automation (roles/gitea/tasks/tls.yml)
- name: Ensure local certs directory exists
ansible.builtin.file:
path: "{{ playbook_dir }}/../vault/generated"
state: directory
mode: '0700'
delegate_to: localhost
run_once: true
become: false
- name: Generate Gitea private key
community.crypto.openssl_privatekey:
path: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.key"
size: 2048
delegate_to: localhost
become: false
- name: Generate Gitea CSR
community.crypto.openssl_csr:
path: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.csr"
privatekey_path: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.key"
common_name: "{{ inventory_hostname }}"
subject_alt_name:
- "IP:{{ ansible_host }}"
- "DNS:{{ inventory_hostname }}"
delegate_to: localhost
become: false
- name: Sign Gitea certificate with Sovereign CA
community.crypto.x509_certificate:
path: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.crt"
csr_path: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.csr"
ownca_path: "{{ playbook_dir }}/../vault/dsom-elasticsearch-ca.crt"
ownca_privatekey_path: "{{ playbook_dir }}/../vault/dsom-elasticsearch-ca.key"
provider: ownca
ownca_not_after: "+1825d"
delegate_to: localhost
become: false
- name: Ensure target cert directory exists
ansible.builtin.file:
path: "~/.config/gitea/certs"
state: directory
mode: '0755'
become: false
- name: Deploy Sovereign CA certificate
ansible.builtin.copy:
src: "{{ playbook_dir }}/../vault/dsom-elasticsearch-ca.crt"
dest: "~/.config/gitea/certs/ca.crt"
mode: '0644'
become: false
- name: Deploy Gitea certificate
ansible.builtin.copy:
src: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.crt"
dest: "~/.config/gitea/certs/gitea.crt"
mode: '0644'
become: false
- name: Deploy Gitea key
ansible.builtin.copy:
src: "{{ playbook_dir }}/../vault/generated/{{ inventory_hostname }}-gitea.key"
dest: "~/.config/gitea/certs/gitea.key"
mode: '0644'
become: false
- name: Install Sovereign CA to Host Trust Store (Debian family)
ansible.builtin.copy:
src: "{{ playbook_dir }}/../vault/dsom-elasticsearch-ca.crt"
dest: /usr/local/share/ca-certificates/sovereign-ca.crt
mode: '0644'
become: true
become_user: root
when: ansible_os_family == 'Debian'
notify: update ca-certificates
- name: Install Sovereign CA to Host Trust Store (Red Hat family)
ansible.builtin.copy:
src: "{{ playbook_dir }}/../vault/dsom-elasticsearch-ca.crt"
dest: /etc/pki/ca-trust/source/anchors/sovereign-ca.crt
mode: '0644'
become: true
become_user: root
when: ansible_os_family == 'RedHat'
notify: update ca-trust
4. Playbook Execution Commands
Deploy Locally on Active Host
ansible-playbook playbooks/dsom/setup_gitea.yml
Deploy Remote Host Inventory
ansible-playbook -i inventory/hosts.yml playbooks/dsom/setup_gitea.yml -e "target_hosts=gitea_production_nodes"
Deploy Using Ansible Vault for Encrypted Passwords
ansible-playbook -i inventory/hosts.yml playbooks/dsom/setup_gitea.yml --ask-vault-pass
5. Post-Deployment Verification
Verify that Gitea systemd service and containers are active on target host:
# Verify systemd user service status
systemctl --user status gitea-stack.service
# Verify podman containers are running
podman pod ps
podman ps
6. Securing Secrets in Ansible & Git (Best Practices)
- Ansible Vault: Always store database and admin credentials in encrypted vault files (
ansible-vault create). - Dynamic Ingestion: Use
lookup('ansible.builtin.env', 'GITEA_DB_PASSWORD')for runtime terminal injection. - Repository Exclusions: Confirm
.gitignoreincludes*.env,*credentials.txt, and private key files (*.key).
Deep State of Mind (DSOM) For My AI Protocol | Harisfazillah Jamel (LinuxMalaysia) | 2026-08-20 Standard: UK English | DBP-standard Bahasa Melayu Malaysia (Piawai) | GNU General Public License v3.0