Skip to content

🛠️ 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 port 3000 over HTTPS)
  • SSH Port: 2222 (Mapped to container port 22 for Git over SSH)
  • Security & TLS: TLS certificates generated via community.crypto and 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)

  1. Ansible Vault: Always store database and admin credentials in encrypted vault files (ansible-vault create).
  2. Dynamic Ingestion: Use lookup('ansible.builtin.env', 'GITEA_DB_PASSWORD') for runtime terminal injection.
  3. Repository Exclusions: Confirm .gitignore includes *.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