Add Template to deploy forgejo.

This template allows deploying a forgejo en either Scaleway or Hetzner
(untested) without much knowledge about them.
It DOES require knowledge about Terragrunt and ansible. A wizard of
sorts is provided but it will not guarantee success without some
knowledge about the underlying technology.
This commit is contained in:
Horacio Duran 2026-01-09 16:07:44 +01:00
parent a9f546f92a
commit 822e42dbb8
48 changed files with 6846 additions and 2 deletions

28
ansible/ansible.cfg Normal file
View file

@ -0,0 +1,28 @@
[defaults]
# Roles path relative to this config file
roles_path = ./roles
# Inventory path
inventory = ./inventory/production/hosts.yml
# Don't create retry files
retry_files_enabled = False
# Show task duration
callback_whitelist = timer
# Reduce verbosity
deprecation_warnings = False
# SSH settings
host_key_checking = False
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
become_user = root
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no

View file

@ -0,0 +1,61 @@
---
# Ansible Inventory for Forgejo Production
# Copy this file to hosts.yml and update with your values:
# cp hosts.yml.example hosts.yml
all:
children:
forgejo:
hosts:
forgejo-prod:
# UPDATE: Your server IP (from terraform output or cloud console)
ansible_host: YOUR_SERVER_IP
ansible_user: root
ansible_port: 22
ansible_python_interpreter: /usr/bin/python3
# =============================================================
# DOMAIN CONFIGURATION (REQUIRED!)
# =============================================================
# UPDATE: Your domain name pointing to the server IP
forgejo_domain: git.example.com
forgejo_version: "9.0.2"
# Database configuration
forgejo_db_type: postgres
forgejo_db_name: forgejo
forgejo_db_user: forgejo
# Enable features
forgejo_enable_letsencrypt: true
forgejo_enable_backups: true
forgejo_enable_2fa: true
forgejo_use_redis: true
# Security settings
forgejo_disable_registration: true # Disable public registration
forgejo_require_signin_view: false # Require login to view repos
# Security: Tailscale VPN + UFW firewall
forgejo_enable_tailscale: true
forgejo_enable_ufw: true
# Email configuration (optional)
forgejo_enable_email: false
# forgejo_email_host: smtp.example.com
# forgejo_email_port: 587
# forgejo_email_user: noreply@example.com
# S3 configuration (optional)
forgejo_enable_s3: false
# forgejo_s3_endpoint: https://s3.example.com
# forgejo_s3_bucket: forgejo-lfs
# forgejo_s3_region: us-east-1
# Backup configuration
forgejo_backup_retention_days: 30
forgejo_backup_to_s3: false
vars:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'

View file

@ -0,0 +1,203 @@
---
# Backup Forgejo data and database
# Creates timestamped backups and optionally uploads to S3
- name: Backup Forgejo
hosts: forgejo
become: yes
gather_facts: yes
vars_files:
- vars/main.yml
- vars/secrets.yml
vars:
backup_timestamp: "{{ ansible_date_time.iso8601_basic_short }}"
backup_filename: "forgejo-backup-{{ backup_timestamp }}.tar.gz"
upload_to_s3: "{{ forgejo_backup_to_s3 | default(false) }}"
pre_tasks:
- name: Display backup information
ansible.builtin.debug:
msg: |
Creating backup: {{ backup_filename }}
Upload to S3: {{ upload_to_s3 }}
Backup path: {{ forgejo_backup_path }}
tasks:
- name: Ensure backup directory exists
ansible.builtin.file:
path: "{{ forgejo_backup_path }}"
state: directory
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0750'
- name: Create PostgreSQL backup
community.postgresql.postgresql_db:
name: "{{ forgejo_db_name }}"
state: dump
target: "{{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql"
become_user: postgres
when: forgejo_db_type == 'postgres'
- name: Compress database backup
community.general.archive:
path: "{{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql"
dest: "{{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql.gz"
format: gz
remove: yes
when: forgejo_db_type == 'postgres'
- name: Create Git repositories backup
community.general.archive:
path: "{{ forgejo_data_path }}/git"
dest: "{{ forgejo_backup_path }}/repositories-{{ backup_timestamp }}.tar.gz"
format: gz
- name: Backup configuration files
community.general.archive:
path:
- "{{ forgejo_config_path }}"
- "{{ forgejo_base_path }}/docker-compose.yml"
dest: "{{ forgejo_backup_path }}/config-{{ backup_timestamp }}.tar.gz"
format: gz
- name: Backup attachments and LFS
community.general.archive:
path:
- "{{ forgejo_data_path }}/attachments"
- "{{ forgejo_data_path }}/lfs"
- "{{ forgejo_data_path }}/avatars"
dest: "{{ forgejo_backup_path }}/data-{{ backup_timestamp }}.tar.gz"
format: gz
- name: Create backup manifest
ansible.builtin.copy:
dest: "{{ forgejo_backup_path }}/manifest-{{ backup_timestamp }}.json"
content: |
{
"timestamp": "{{ ansible_date_time.iso8601 }}",
"version": "{{ forgejo_version }}",
"hostname": "{{ ansible_hostname }}",
"database": "{{ forgejo_db_type }}",
"files": {
"database": "database-{{ backup_timestamp }}.sql.gz",
"repositories": "repositories-{{ backup_timestamp }}.tar.gz",
"config": "config-{{ backup_timestamp }}.tar.gz",
"data": "data-{{ backup_timestamp }}.tar.gz"
},
"sizes": {}
}
mode: '0644'
- name: Get backup file sizes
ansible.builtin.stat:
path: "{{ forgejo_backup_path }}/{{ item }}"
register: backup_files
loop:
- "database-{{ backup_timestamp }}.sql.gz"
- "repositories-{{ backup_timestamp }}.tar.gz"
- "config-{{ backup_timestamp }}.tar.gz"
- "data-{{ backup_timestamp }}.tar.gz"
- name: Display backup sizes
ansible.builtin.debug:
msg: "{{ item.item }}: {{ (item.stat.size / 1024 / 1024) | round(2) }} MB"
loop: "{{ backup_files.results }}"
when: item.stat.exists
- name: Upload to S3
when: upload_to_s3 and forgejo_enable_s3
block:
- name: Install AWS CLI
ansible.builtin.pip:
name: awscli
state: present
- name: Upload database backup to S3
ansible.builtin.command:
cmd: >
aws s3 cp {{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql.gz
s3://{{ forgejo_backup_s3_bucket }}/backups/database-{{ backup_timestamp }}.sql.gz
--endpoint-url {{ forgejo_s3_endpoint }}
environment:
AWS_ACCESS_KEY_ID: "{{ forgejo_s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ forgejo_s3_secret_key }}"
no_log: yes
- name: Upload repositories backup to S3
ansible.builtin.command:
cmd: >
aws s3 cp {{ forgejo_backup_path }}/repositories-{{ backup_timestamp }}.tar.gz
s3://{{ forgejo_backup_s3_bucket }}/backups/repositories-{{ backup_timestamp }}.tar.gz
--endpoint-url {{ forgejo_s3_endpoint }}
environment:
AWS_ACCESS_KEY_ID: "{{ forgejo_s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ forgejo_s3_secret_key }}"
no_log: yes
- name: Upload config backup to S3
ansible.builtin.command:
cmd: >
aws s3 cp {{ forgejo_backup_path }}/config-{{ backup_timestamp }}.tar.gz
s3://{{ forgejo_backup_s3_bucket }}/backups/config-{{ backup_timestamp }}.tar.gz
--endpoint-url {{ forgejo_s3_endpoint }}
environment:
AWS_ACCESS_KEY_ID: "{{ forgejo_s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ forgejo_s3_secret_key }}"
no_log: yes
- name: Upload manifest to S3
ansible.builtin.command:
cmd: >
aws s3 cp {{ forgejo_backup_path }}/manifest-{{ backup_timestamp }}.json
s3://{{ forgejo_backup_s3_bucket }}/backups/manifest-{{ backup_timestamp }}.json
--endpoint-url {{ forgejo_s3_endpoint }}
environment:
AWS_ACCESS_KEY_ID: "{{ forgejo_s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ forgejo_s3_secret_key }}"
no_log: yes
- name: Clean up old backups
ansible.builtin.shell: |
find {{ forgejo_backup_path }} -name "*.tar.gz" -o -name "*.sql.gz" -o -name "*.json" | \
grep -E "[0-9]{8}T[0-9]{6}" | \
sort -r | \
tail -n +{{ (forgejo_backup_retention_days | int * 4) + 1 }} | \
xargs -r rm -f
args:
executable: /bin/bash
when: forgejo_backup_retention_days is defined
post_tasks:
- name: Calculate total backup size
ansible.builtin.shell: |
du -sh {{ forgejo_backup_path }} | cut -f1
register: total_backup_size
changed_when: false
- name: Display completion message
ansible.builtin.debug:
msg: |
========================================
Backup Complete!
========================================
Timestamp: {{ backup_timestamp }}
Location: {{ forgejo_backup_path }}
Total size: {{ total_backup_size.stdout }}
Files created:
- database-{{ backup_timestamp }}.sql.gz
- repositories-{{ backup_timestamp }}.tar.gz
- config-{{ backup_timestamp }}.tar.gz
- data-{{ backup_timestamp }}.tar.gz
- manifest-{{ backup_timestamp }}.json
{% if upload_to_s3 %}
Uploaded to S3: {{ forgejo_backup_s3_bucket }}/backups/
{% endif %}
Retention: {{ forgejo_backup_retention_days }} days
========================================

View file

@ -0,0 +1,122 @@
---
# Deploy Forgejo Git Forge
# This playbook deploys a complete Forgejo instance with PostgreSQL, Redis, Nginx, and SSL
- name: Deploy Forgejo
hosts: forgejo
become: yes
gather_facts: yes
vars_files:
- vars/main.yml
- vars/secrets.yml # Ansible Vault encrypted
pre_tasks:
- name: Verify Ansible version
ansible.builtin.assert:
that:
- ansible_version.full is version('2.14', '>=')
fail_msg: "This playbook requires Ansible 2.14 or higher"
success_msg: "Ansible version is compatible"
- name: Gather system facts
ansible.builtin.setup:
- name: Check system requirements
ansible.builtin.assert:
that:
- ansible_memtotal_mb >= 3500
- ansible_processor_vcpus >= 2
fail_msg: "System does not meet minimum requirements (4GB RAM, 2 vCPUs)"
success_msg: "System meets requirements"
- name: Display deployment information
ansible.builtin.debug:
msg: |
Deploying Forgejo {{ forgejo_version }}
Domain: {{ forgejo_domain }}
Database: {{ forgejo_db_type }}
HTTPS: {{ forgejo_enable_letsencrypt }}
S3: {{ forgejo_enable_s3 }}
roles:
- role: forgejo
tags: ['forgejo']
post_tasks:
- name: Display completion message
ansible.builtin.debug:
msg: |
========================================
Forgejo Deployment Complete!
========================================
Access your Forgejo instance at:
{{ forgejo_protocol }}://{{ forgejo_domain }}
SSH clone URL:
git@{{ forgejo_domain }}:{{ forgejo_ssh_port }}
Admin credentials (if first install):
Username: {{ forgejo_admin_username }}
Password: (set in vault)
Next steps:
1. Visit the web interface and complete setup
2. Configure OAuth/LDAP if needed
3. Set up CI/CD with Forgejo Actions
4. Configure webhooks for integrations
Backup location: {{ forgejo_backup_path }}
Logs: {{ forgejo_data_path }}/gitea/log
========================================
- name: Verify Forgejo is running
ansible.builtin.uri:
url: "http://localhost:{{ forgejo_http_port }}"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 5
delay: 3
- name: Create deployment summary file
ansible.builtin.copy:
dest: "{{ forgejo_base_path }}/DEPLOYMENT_INFO.txt"
content: |
Forgejo Deployment Information
==============================
Deployment Date: {{ ansible_date_time.iso8601 }}
Forgejo Version: {{ forgejo_version }}
Ansible User: {{ ansible_user }}
Server Details:
- Hostname: {{ ansible_hostname }}
- IP Address: {{ ansible_default_ipv4.address }}
- OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
- RAM: {{ ansible_memtotal_mb }} MB
- CPUs: {{ ansible_processor_vcpus }}
Configuration:
- Domain: {{ forgejo_domain }}
- HTTP Port: {{ forgejo_http_port }}
- SSH Port: {{ forgejo_ssh_port }}
- Database: {{ forgejo_db_type }}
- Redis: {{ forgejo_use_redis }}
- LFS: {{ forgejo_enable_lfs }}
Paths:
- Base: {{ forgejo_base_path }}
- Data: {{ forgejo_data_path }}
- Config: {{ forgejo_config_path }}
- Backups: {{ forgejo_backup_path }}
Maintenance Commands:
- Restart: docker compose -f {{ forgejo_base_path }}/docker-compose.yml restart
- Logs: docker logs forgejo
- Backup: /usr/local/bin/forgejo_backup.sh
- Update: docker compose -f {{ forgejo_base_path }}/docker-compose.yml pull && docker compose up -d
mode: '0644'
become: yes

View file

@ -0,0 +1,234 @@
---
# Restore Forgejo from backup
# Restores database, repositories, configuration, and data
- name: Restore Forgejo from Backup
hosts: forgejo
become: yes
gather_facts: yes
vars_files:
- vars/main.yml
- vars/secrets.yml
vars:
# Must be provided via --extra-vars
backup_timestamp: ""
backup_source: "local" # local or s3
force_restore: false
pre_tasks:
- name: Validate backup timestamp
ansible.builtin.fail:
msg: "Please provide backup_timestamp via --extra-vars 'backup_timestamp=20240115T120000'"
when: backup_timestamp == ""
- name: Display restore information
ansible.builtin.debug:
msg: |
========================================
WARNING: This will restore Forgejo data
========================================
Backup timestamp: {{ backup_timestamp }}
Source: {{ backup_source }}
This operation will:
1. Stop Forgejo service
2. Restore database
3. Restore repositories
4. Restore configuration
5. Restart services
Current data will be backed up first.
- name: Confirm restore operation
ansible.builtin.pause:
prompt: "Type 'yes' to continue with restore"
register: restore_confirm
when: not force_restore
- name: Validate confirmation
ansible.builtin.fail:
msg: "Restore cancelled by user"
when: not force_restore and restore_confirm.user_input != 'yes'
tasks:
- name: Create pre-restore backup
ansible.builtin.include_tasks: backup.yml
vars:
backup_filename: "pre-restore-{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
- name: Download backup from S3 if needed
when: backup_source == 's3'
block:
- name: Create temporary download directory
ansible.builtin.file:
path: "{{ forgejo_backup_path }}/restore-temp"
state: directory
mode: '0750'
- name: Download backups from S3
ansible.builtin.command:
cmd: >
aws s3 cp s3://{{ forgejo_backup_s3_bucket }}/backups/{{ item }}-{{ backup_timestamp }}.tar.gz
{{ forgejo_backup_path }}/{{ item }}-{{ backup_timestamp }}.tar.gz
--endpoint-url {{ forgejo_s3_endpoint }}
environment:
AWS_ACCESS_KEY_ID: "{{ forgejo_s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ forgejo_s3_secret_key }}"
loop:
- database
- repositories
- config
- data
no_log: yes
- name: Verify backup files exist
ansible.builtin.stat:
path: "{{ forgejo_backup_path }}/{{ item }}-{{ backup_timestamp }}.tar.gz"
register: backup_files
loop:
- repositories
- config
- data
failed_when: not backup_files.results | map(attribute='stat.exists') | list | min
- name: Verify database backup exists
ansible.builtin.stat:
path: "{{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql.gz"
register: db_backup
failed_when: not db_backup.stat.exists
- name: Stop Forgejo service
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
state: stopped
- name: Restore PostgreSQL database
when: forgejo_db_type == 'postgres'
block:
- name: Drop existing database
community.postgresql.postgresql_db:
name: "{{ forgejo_db_name }}"
state: absent
become_user: postgres
- name: Recreate database
community.postgresql.postgresql_db:
name: "{{ forgejo_db_name }}"
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
template: template0
state: present
become_user: postgres
- name: Decompress database backup
ansible.builtin.command:
cmd: gunzip -c {{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql.gz
register: db_dump
- name: Restore database
community.postgresql.postgresql_db:
name: "{{ forgejo_db_name }}"
state: restore
target: "{{ forgejo_backup_path }}/database-{{ backup_timestamp }}.sql"
become_user: postgres
- name: Clear existing repositories
ansible.builtin.file:
path: "{{ forgejo_data_path }}/git"
state: absent
- name: Restore repositories
ansible.builtin.unarchive:
src: "{{ forgejo_backup_path }}/repositories-{{ backup_timestamp }}.tar.gz"
dest: "{{ forgejo_data_path }}"
remote_src: yes
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
- name: Restore configuration
ansible.builtin.unarchive:
src: "{{ forgejo_backup_path }}/config-{{ backup_timestamp }}.tar.gz"
dest: "{{ forgejo_base_path }}"
remote_src: yes
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
- name: Restore data files
ansible.builtin.unarchive:
src: "{{ forgejo_backup_path }}/data-{{ backup_timestamp }}.tar.gz"
dest: "{{ forgejo_data_path }}"
remote_src: yes
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
- name: Set correct permissions
ansible.builtin.file:
path: "{{ item }}"
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
recurse: yes
loop:
- "{{ forgejo_data_path }}/git"
- "{{ forgejo_data_path }}/attachments"
- "{{ forgejo_data_path }}/lfs"
- "{{ forgejo_config_path }}"
- name: Start Forgejo service
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
state: present
- name: Wait for Forgejo to be ready
ansible.builtin.uri:
url: "http://localhost:{{ forgejo_http_port }}"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 2
- name: Run integrity checks
ansible.builtin.command:
cmd: docker exec forgejo forgejo doctor check --all
register: integrity_check
failed_when: false
- name: Display integrity check results
ansible.builtin.debug:
msg: "{{ integrity_check.stdout_lines }}"
post_tasks:
- name: Verify Forgejo health
ansible.builtin.uri:
url: "http://localhost:{{ forgejo_http_port }}/api/healthz"
status_code: 200
register: health
- name: Clean up temporary files
ansible.builtin.file:
path: "{{ forgejo_backup_path }}/restore-temp"
state: absent
when: backup_source == 's3'
- name: Display completion message
ansible.builtin.debug:
msg: |
========================================
Restore Complete!
========================================
Restored from backup: {{ backup_timestamp }}
Forgejo is now running with restored data.
Please verify:
1. Login works correctly
2. Repositories are accessible
3. All data is present
Original data was backed up before restore.
========================================

View file

@ -0,0 +1,133 @@
---
# Update Forgejo to latest version
# This playbook safely updates Forgejo with automatic backup
- name: Update Forgejo
hosts: forgejo
become: yes
gather_facts: yes
vars_files:
- vars/main.yml
- vars/secrets.yml
vars:
backup_before_update: true
skip_backup: false # Override with --extra-vars "skip_backup=true"
pre_tasks:
- name: Display update information
ansible.builtin.debug:
msg: |
Updating Forgejo from {{ forgejo_version }}
Backup will be created: {{ backup_before_update and not skip_backup }}
- name: Check current Forgejo version
ansible.builtin.command:
cmd: docker exec forgejo forgejo --version
register: current_version
changed_when: false
failed_when: false
- name: Display current version
ansible.builtin.debug:
msg: "Current version: {{ current_version.stdout if current_version.rc == 0 else 'Unable to determine' }}"
tasks:
- name: Create pre-update backup
when: backup_before_update and not skip_backup
block:
- name: Run backup script
ansible.builtin.command:
cmd: /usr/local/bin/forgejo_backup.sh
register: backup_result
- name: Display backup result
ansible.builtin.debug:
msg: "Backup completed: {{ backup_result.stdout_lines[-1] if backup_result.stdout_lines else 'No output' }}"
- name: Stop Forgejo service
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
state: stopped
- name: Pull latest Forgejo image
community.docker.docker_image:
name: "{{ forgejo_docker_image }}:{{ forgejo_version }}"
source: pull
force_source: yes
- name: Update Docker Compose file if needed
ansible.builtin.template:
src: ../roles/forgejo/templates/docker-compose.yml.j2
dest: "{{ forgejo_base_path }}/docker-compose.yml"
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0640'
- name: Start Forgejo service
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
state: present
pull: always
- name: Wait for Forgejo to be ready
ansible.builtin.uri:
url: "http://localhost:{{ forgejo_http_port }}"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 2
- name: Check updated version
ansible.builtin.command:
cmd: docker exec forgejo forgejo --version
register: updated_version
changed_when: false
- name: Display updated version
ansible.builtin.debug:
msg: "Updated version: {{ updated_version.stdout }}"
- name: Run database migrations
ansible.builtin.command:
cmd: docker exec forgejo forgejo migrate
register: migrate_result
changed_when: "'No migration needed' not in migrate_result.stdout"
- name: Display migration result
ansible.builtin.debug:
msg: "{{ migrate_result.stdout_lines }}"
post_tasks:
- name: Verify Forgejo health
ansible.builtin.uri:
url: "http://localhost:{{ forgejo_http_port }}/api/healthz"
status_code: 200
return_content: yes
register: health
- name: Display health status
ansible.builtin.debug:
msg: "Forgejo health check: {{ health.content }}"
- name: Update deployment info
ansible.builtin.lineinfile:
path: "{{ forgejo_base_path }}/DEPLOYMENT_INFO.txt"
regexp: '^Last Update:'
line: "Last Update: {{ ansible_date_time.iso8601 }} - {{ forgejo_version }}"
insertafter: '^Deployment Date:'
- name: Display completion message
ansible.builtin.debug:
msg: |
========================================
Forgejo Update Complete!
========================================
Previous version: {{ current_version.stdout if current_version.rc == 0 else 'Unknown' }}
Current version: {{ updated_version.stdout }}
The service is running and healthy.
========================================

View file

@ -0,0 +1,64 @@
---
# Main variables for Forgejo deployment
# NOTE: Domain-specific settings should be in inventory/production/hosts.yml
# Variables here are lower-priority defaults only.
# Forgejo version (can be overridden in inventory)
# forgejo_version: "9.0.2"
# Protocol for public URLs (https recommended)
forgejo_protocol: https
# System configuration
forgejo_user: git
forgejo_group: git
forgejo_uid: 1100
forgejo_gid: 1100
# Installation paths
forgejo_base_path: /opt/forgejo
forgejo_data_path: "{{ forgejo_base_path }}/data"
forgejo_config_path: "{{ forgejo_base_path }}/config"
# Network ports
forgejo_http_port: 3000
forgejo_ssh_port: 2222
# Database configuration
forgejo_db_type: postgres
forgejo_db_host: localhost
forgejo_db_port: 5432
forgejo_db_name: forgejo
forgejo_db_user: forgejo
# Redis configuration
forgejo_use_redis: true
redis_host: localhost
redis_port: 6379
# SSL/TLS configuration
forgejo_enable_letsencrypt: true
letsencrypt_email: "admin@{{ forgejo_domain }}"
# Features (can be overridden in inventory)
forgejo_enable_lfs: true
forgejo_enable_2fa: true
# forgejo_disable_registration - set in inventory
# forgejo_require_signin_view - set in inventory
# Backup configuration
forgejo_enable_backups: true
forgejo_backup_schedule: "0 2 * * *"
forgejo_backup_retention_days: 30
# Monitoring
forgejo_enable_prometheus: false
# Email configuration
forgejo_enable_email: false
# S3 configuration
forgejo_enable_s3: false
# Security
forgejo_log_level: Info

View file

@ -0,0 +1,40 @@
---
# Ansible Vault Encrypted Secrets
#
# IMPORTANT: Do NOT commit secrets.yml to git, even if encrypted!
# The .gitignore is configured to exclude it, but always verify.
#
# To set up:
# 1. cp secrets.yml.example secrets.yml
# 2. Edit secrets.yml with your actual values
# 3. ansible-vault encrypt secrets.yml
# 4. Verify: git status should NOT show secrets.yml
#
# To edit encrypted secrets: ansible-vault edit secrets.yml
# Database passwords
vault_forgejo_db_password: "CHANGE_ME_STRONG_PASSWORD_HERE"
# Admin account
vault_forgejo_admin_password: "CHANGE_ME_ADMIN_PASSWORD_HERE"
# Secret keys (generate with: openssl rand -base64 32)
vault_forgejo_secret_key: "CHANGE_ME_SECRET_KEY_64_CHARS_MINIMUM_XXXXXXXXXXXXXXXXX"
vault_forgejo_internal_token: "CHANGE_ME_INTERNAL_TOKEN_XXXXXXXXXXXXXXXXXXXXXXXXX"
vault_forgejo_jwt_secret: "CHANGE_ME_JWT_SECRET_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Metrics token (if prometheus enabled)
vault_forgejo_metrics_token: "CHANGE_ME_METRICS_TOKEN_XXXXXXXXX"
# Email password (if email enabled)
vault_email_password: ""
# S3 credentials (if S3 enabled)
vault_s3_access_key: ""
vault_s3_secret_key: ""
# Notes:
# - Generate strong passwords: openssl rand -base64 32
# - Never commit unencrypted secrets to version control
# - Keep a secure backup of your vault password
# - Rotate secrets regularly

View file

@ -0,0 +1,145 @@
---
# Default variables for Forgejo role
# Forgejo version
forgejo_version: "9.0.2"
forgejo_docker_image: "codeberg.org/forgejo/forgejo"
# System user and group
forgejo_user: git
forgejo_group: git
forgejo_uid: 1100
forgejo_gid: 1100
# Installation paths
forgejo_base_path: /opt/forgejo
forgejo_data_path: "{{ forgejo_base_path }}/data"
forgejo_config_path: "{{ forgejo_base_path }}/config"
forgejo_custom_path: "{{ forgejo_base_path }}/custom"
forgejo_backup_path: "{{ forgejo_base_path }}/backups"
# External volume (if using cloud provider block storage)
forgejo_use_external_volume: false
forgejo_volume_device: /dev/sdb
forgejo_volume_mount: /mnt/forgejo-data
# Network configuration
# NOTE: Set your actual domain in ansible/inventory/production/hosts.yml
# The value here is just a fallback default.
forgejo_domain: git.example.com
forgejo_http_port: 3000
forgejo_ssh_port: 2222
forgejo_protocol: https
# Database configuration
forgejo_db_type: postgres
forgejo_db_host: localhost
forgejo_db_port: 5432
forgejo_db_name: forgejo
forgejo_db_user: forgejo
forgejo_db_password: "{{ vault_forgejo_db_password | default('changeme') }}"
# PostgreSQL settings
postgres_version: "16"
postgres_data_dir: "{{ forgejo_data_path }}/postgres"
postgres_max_connections: 100
postgres_shared_buffers: "256MB"
postgres_effective_cache_size: "1GB"
# Redis configuration (optional, for caching)
forgejo_use_redis: true
redis_host: localhost
redis_port: 6379
# Admin user (created on first setup)
# NOTE: "admin" is a reserved name in Forgejo, use something else
forgejo_admin_username: forgejo_admin
forgejo_admin_password: "{{ vault_forgejo_admin_password | default('changeme') }}"
forgejo_admin_email: "admin@{{ forgejo_domain }}"
# HTTPS/SSL configuration
forgejo_enable_letsencrypt: true
letsencrypt_email: "admin@{{ forgejo_domain }}"
certbot_create_if_missing: true
certbot_auto_renew: true
# Object storage (S3-compatible)
forgejo_enable_s3: false
forgejo_s3_endpoint: ""
forgejo_s3_bucket: ""
forgejo_s3_region: ""
forgejo_s3_access_key: "{{ vault_s3_access_key | default('') }}"
forgejo_s3_secret_key: "{{ vault_s3_secret_key | default('') }}"
# Backup configuration
forgejo_enable_backups: true
forgejo_backup_schedule: "0 2 * * *" # Daily at 2 AM
forgejo_backup_retention_days: 30
forgejo_backup_to_s3: false
forgejo_backup_s3_bucket: ""
# Security settings
forgejo_disable_registration: false
forgejo_require_signin_view: false
forgejo_enable_2fa: true
# Tailscale VPN configuration
# Recommended: Enable for secure SSH access
forgejo_enable_tailscale: true
tailscale_interface: tailscale0
# UFW Firewall configuration
# When enabled with Tailscale, SSH is only accessible via Tailscale
forgejo_enable_ufw: true
ufw_reset_on_configure: false # Set to true to reset all rules before configuring
# Email configuration (optional)
forgejo_enable_email: false
forgejo_email_host: ""
forgejo_email_port: 587
forgejo_email_user: ""
forgejo_email_password: "{{ vault_email_password | default('') }}"
forgejo_email_from: "noreply@{{ forgejo_domain }}"
# Git configuration
forgejo_disable_http_git: false
forgejo_enable_lfs: true
forgejo_lfs_max_file_size: 100 # MB
# Performance tuning
forgejo_log_level: Info
forgejo_disable_gravatar: false
# Docker Compose configuration
docker_compose_version: "2.24.0"
docker_install_compose: true
# Firewall configuration
firewall_allowed_tcp_ports:
- "22"
- "80"
- "443"
- "{{ forgejo_ssh_port }}"
# System packages to install
system_packages:
- curl
- wget
- git
- htop
- vim
- tmux
- unzip
- jq
- python3-pip
- python3-docker
- python3-psycopg2
- acl
# Monitoring (optional)
forgejo_enable_prometheus: false
prometheus_port: 9090
# Restore from backup
forgejo_restore_from_backup: false
forgejo_restore_backup_file: ""

View file

@ -0,0 +1,81 @@
---
# Handlers for Forgejo role
- name: Restart Docker
ansible.builtin.systemd:
name: docker
state: restarted
daemon_reload: yes
become: yes
- name: Restart Forgejo
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
state: restarted
become: yes
- name: Reload Forgejo
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
reloaded: yes
become: yes
- name: Restart PostgreSQL
ansible.builtin.systemd:
name: postgresql
state: restarted
become: yes
when: forgejo_db_type == 'postgres'
- name: Restart Redis
ansible.builtin.systemd:
name: redis-server
state: restarted
become: yes
when: forgejo_use_redis | bool
- name: Reload Caddy
ansible.builtin.systemd:
name: caddy
state: reloaded
become: yes
- name: Restart Caddy
ansible.builtin.systemd:
name: caddy
state: restarted
become: yes
- name: Reload Systemd
ansible.builtin.systemd:
daemon_reload: yes
become: yes
- name: Restart UFW
ansible.builtin.systemd:
name: ufw
state: restarted
become: yes
- name: Restart sshd
ansible.builtin.systemd:
name: sshd
state: restarted
become: yes
- name: Renew SSL Certificate
ansible.builtin.debug:
msg: "Caddy handles certificate renewal automatically - no manual action needed"
when: forgejo_enable_letsencrypt | bool
- name: Restart Prometheus
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
files:
- docker-compose.yml
- docker-compose.monitoring.yml
services:
- prometheus
state: restarted
become: yes
when: forgejo_enable_prometheus | bool

View file

@ -0,0 +1,32 @@
---
# Backup configuration tasks
- name: Create backup script
ansible.builtin.template:
src: forgejo_backup.sh.j2
dest: /usr/local/bin/forgejo_backup.sh
mode: '0755'
become: yes
- name: Set up backup cron job
ansible.builtin.cron:
name: "Forgejo daily backup"
minute: "{{ forgejo_backup_schedule.split()[0] }}"
hour: "{{ forgejo_backup_schedule.split()[1] }}"
job: "/usr/local/bin/forgejo_backup.sh >> /var/log/forgejo-backup.log 2>&1"
become: yes
- name: Create log rotation for backup logs
ansible.builtin.copy:
dest: /etc/logrotate.d/forgejo-backup
content: |
/var/log/forgejo-backup.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
}
mode: '0644'
become: yes

View file

@ -0,0 +1,71 @@
---
# Caddy web server setup tasks
# Caddy handles HTTPS certificates automatically via Let's Encrypt
- name: Install dependencies for Caddy
ansible.builtin.apt:
name:
- debian-keyring
- debian-archive-keyring
- apt-transport-https
- curl
state: present
update_cache: yes
become: yes
- name: Add Caddy GPG key
ansible.builtin.shell: |
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
args:
creates: /usr/share/keyrings/caddy-stable-archive-keyring.gpg
become: yes
- name: Add Caddy repository
ansible.builtin.shell: |
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
args:
creates: /etc/apt/sources.list.d/caddy-stable.list
become: yes
- name: Install Caddy
ansible.builtin.apt:
name: caddy
state: present
update_cache: yes
become: yes
- name: Create Caddy configuration directory
ansible.builtin.file:
path: /etc/caddy
state: directory
owner: root
group: root
mode: '0755'
become: yes
- name: Create Caddy log directory
ansible.builtin.file:
path: /var/log/caddy
state: directory
owner: caddy
group: caddy
mode: '0755'
become: yes
- name: Create Caddyfile for Forgejo
ansible.builtin.template:
src: Caddyfile.j2
dest: /etc/caddy/Caddyfile
owner: root
group: root
mode: '0644'
validate: 'caddy validate --adapter caddyfile --config %s'
become: yes
notify: Reload Caddy
- name: Ensure Caddy is started and enabled
ansible.builtin.systemd:
name: caddy
state: started
enabled: yes
become: yes

View file

@ -0,0 +1,100 @@
---
# Docker installation tasks
- name: Check if Docker is already installed
ansible.builtin.command: docker --version
register: docker_installed
changed_when: false
failed_when: false
- name: Install Docker
when: docker_installed.rc != 0
block:
- name: Install Docker dependencies
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: yes
become: yes
- name: Create directory for Docker GPG key
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
become: yes
- name: Add Docker GPG key
ansible.builtin.apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
keyring: /etc/apt/keyrings/docker.gpg
state: present
become: yes
- name: Add Docker repository
ansible.builtin.apt_repository:
repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
filename: docker
become: yes
- name: Install Docker Engine
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: yes
become: yes
- name: Add Forgejo user to Docker group
ansible.builtin.user:
name: "{{ forgejo_user }}"
groups: docker
append: yes
become: yes
- name: Ensure Docker service is started and enabled
ansible.builtin.systemd:
name: docker
state: started
enabled: yes
daemon_reload: yes
become: yes
- name: Configure Docker daemon
ansible.builtin.copy:
dest: /etc/docker/daemon.json
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"userland-proxy": false,
"live-restore": true
}
mode: '0644'
become: yes
notify: Restart Docker
- name: Verify Docker installation
ansible.builtin.command: docker run --rm hello-world
register: docker_test
changed_when: false
become: yes
- name: Display Docker version
ansible.builtin.debug:
msg: "Docker is installed and working"
when: docker_test.rc == 0

View file

@ -0,0 +1,138 @@
---
# Forgejo deployment tasks
- name: Ensure Forgejo data directories have correct ownership
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ forgejo_uid }}"
group: "{{ forgejo_gid }}"
mode: '0755'
recurse: yes
become: yes
loop:
- "{{ forgejo_data_path }}"
- "{{ forgejo_config_path }}"
- "{{ forgejo_custom_path }}"
- name: Create .ssh directory for Forgejo
ansible.builtin.file:
path: "{{ forgejo_data_path }}/git/.ssh"
state: directory
owner: "{{ forgejo_uid }}"
group: "{{ forgejo_gid }}"
mode: '0700'
become: yes
- name: Create Forgejo configuration from template
ansible.builtin.template:
src: app.ini.j2
dest: "{{ forgejo_config_path }}/app.ini"
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0640'
become: yes
notify: Restart Forgejo
- name: Create Docker Compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ forgejo_base_path }}/docker-compose.yml"
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0640'
become: yes
notify: Restart Forgejo
- name: Pull Forgejo Docker image
community.docker.docker_image:
name: "{{ forgejo_docker_image }}:{{ forgejo_version }}"
source: pull
become: yes
- name: Start Forgejo with Docker Compose
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
state: present
become: yes
register: forgejo_started
- name: Wait for Forgejo to be ready
ansible.builtin.uri:
url: "http://localhost:{{ forgejo_http_port }}"
status_code: 200
register: forgejo_health
until: forgejo_health.status == 200
retries: 30
delay: 5
ignore_errors: yes
- name: Get Forgejo container logs if startup failed
ansible.builtin.command:
cmd: docker logs forgejo --tail 50
register: forgejo_logs
become: yes
when: forgejo_health.status is not defined or forgejo_health.status != 200
- name: Show Forgejo container logs
ansible.builtin.debug:
var: forgejo_logs.stdout_lines
when: forgejo_logs is defined and forgejo_logs.stdout_lines is defined
- name: Fail if Forgejo is not ready
ansible.builtin.fail:
msg: "Forgejo failed to start. Check logs above."
when: forgejo_health.status is not defined or forgejo_health.status != 200
- name: Check if admin user exists
ansible.builtin.command:
cmd: docker exec --user git forgejo forgejo admin user list --admin
register: admin_user_check
become: yes
changed_when: false
failed_when: false
- name: Create admin user
ansible.builtin.command:
cmd: >
docker exec --user git forgejo forgejo admin user create
--admin
--username "{{ forgejo_admin_username }}"
--password "{{ forgejo_admin_password }}"
--email "{{ forgejo_admin_email }}"
--must-change-password=false
become: yes
when: forgejo_admin_username not in admin_user_check.stdout
register: admin_created
no_log: yes
- name: Display admin credentials
ansible.builtin.debug:
msg: |
=====================================================
ADMIN USER CREATED
=====================================================
Username: {{ forgejo_admin_username }}
Email: {{ forgejo_admin_email }}
Password: (from your secrets.yml vault)
IMPORTANT: Change this password after first login!
=====================================================
when: admin_created is defined and admin_created.changed
- name: Create Forgejo systemd service
ansible.builtin.template:
src: forgejo.service.j2
dest: /etc/systemd/system/forgejo.service
mode: '0644'
become: yes
notify:
- Reload Systemd
- Restart Forgejo
- name: Enable Forgejo service
ansible.builtin.systemd:
name: forgejo
enabled: yes
daemon_reload: yes
become: yes

View file

@ -0,0 +1,94 @@
---
# Main tasks for Forgejo deployment
- name: Include system preparation tasks
ansible.builtin.include_tasks: prepare.yml
tags:
- prepare
- system
- name: Include Tailscale VPN setup tasks
ansible.builtin.include_tasks: tailscale.yml
when: forgejo_enable_tailscale | bool
tags:
- tailscale
- security
- vpn
- name: Include volume setup tasks
ansible.builtin.include_tasks: volume.yml
when: forgejo_use_external_volume | bool
tags:
- volume
- storage
- name: Include Docker installation tasks
ansible.builtin.include_tasks: docker.yml
tags:
- docker
- install
- name: Include PostgreSQL setup tasks
ansible.builtin.include_tasks: postgres.yml
when: forgejo_db_type == 'postgres'
tags:
- postgres
- database
- name: Include Redis setup tasks
ansible.builtin.include_tasks: redis.yml
when: forgejo_use_redis | bool
tags:
- redis
- cache
# Ensure PostgreSQL is restarted with new config before Forgejo connects
- name: Flush handlers before starting Forgejo
ansible.builtin.meta: flush_handlers
- name: Include Forgejo configuration tasks
ansible.builtin.include_tasks: forgejo.yml
tags:
- forgejo
- config
- name: Include Caddy setup tasks
ansible.builtin.include_tasks: caddy.yml
tags:
- caddy
- webserver
- name: Include SSL certificate tasks
ansible.builtin.include_tasks: ssl.yml
when: forgejo_enable_letsencrypt | bool
tags:
- ssl
- certificates
- name: Include backup configuration tasks
ansible.builtin.include_tasks: backup.yml
when: forgejo_enable_backups | bool
tags:
- backup
- name: Include restore tasks
ansible.builtin.include_tasks: restore.yml
when: forgejo_restore_from_backup | bool
tags:
- restore
- never # Only run when explicitly requested
- name: Include monitoring setup tasks
ansible.builtin.include_tasks: monitoring.yml
when: forgejo_enable_prometheus | bool
tags:
- monitoring
- prometheus
- name: Include UFW firewall configuration tasks
ansible.builtin.include_tasks: ufw.yml
when: forgejo_enable_ufw | bool
tags:
- ufw
- firewall
- security

View file

@ -0,0 +1,66 @@
---
# Prometheus monitoring setup for Forgejo
# This is INTERNAL monitoring - metrics are only accessible locally or via authenticated endpoint
- name: Create monitoring directory
ansible.builtin.file:
path: "{{ forgejo_base_path }}/monitoring"
state: directory
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0755'
become: yes
- name: Create Prometheus configuration
ansible.builtin.template:
src: prometheus.yml.j2
dest: "{{ forgejo_base_path }}/monitoring/prometheus.yml"
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0644'
become: yes
notify: Restart Prometheus
- name: Create Prometheus Docker Compose override
ansible.builtin.template:
src: docker-compose.monitoring.yml.j2
dest: "{{ forgejo_base_path }}/docker-compose.monitoring.yml"
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0644'
become: yes
notify: Restart Prometheus
- name: Create Prometheus data directory
ansible.builtin.file:
path: "{{ forgejo_base_path }}/monitoring/data"
state: directory
owner: "65534" # nobody user in Prometheus container
group: "65534"
mode: '0755'
become: yes
- name: Start Prometheus container
community.docker.docker_compose_v2:
project_src: "{{ forgejo_base_path }}"
files:
- docker-compose.yml
- docker-compose.monitoring.yml
state: present
become: yes
- name: Display monitoring access information
ansible.builtin.debug:
msg: |
Prometheus monitoring is now enabled!
Internal access (from server):
- Prometheus UI: http://localhost:9090
- Forgejo metrics: http://localhost:3000/metrics (requires token)
The metrics endpoint is protected by a token configured in your secrets.yml
(vault_forgejo_metrics_token). Use this token in the Authorization header
or as a query parameter: /metrics?token=YOUR_TOKEN
Prometheus scrapes Forgejo metrics every 15 seconds.
Data is retained for 15 days by default.

View file

@ -0,0 +1,163 @@
---
# PostgreSQL setup tasks
- name: Install PostgreSQL
ansible.builtin.apt:
name:
- "postgresql-{{ postgres_version }}"
- "postgresql-contrib-{{ postgres_version }}"
- python3-psycopg2
state: present
update_cache: yes
become: yes
- name: Ensure PostgreSQL is started and enabled
ansible.builtin.systemd:
name: postgresql
state: started
enabled: yes
become: yes
- name: Create PostgreSQL data directory
ansible.builtin.file:
path: "{{ postgres_data_dir }}"
state: directory
owner: postgres
group: postgres
mode: '0700'
become: yes
when: forgejo_use_external_volume | bool
- name: Check if PostgreSQL database exists
ansible.builtin.command:
cmd: psql -U postgres -lqt
register: postgres_db_list
changed_when: false
become: yes
become_user: postgres
- name: Create Forgejo PostgreSQL database
community.postgresql.postgresql_db:
name: "{{ forgejo_db_name }}"
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
template: template0
state: present
become: yes
become_user: postgres
when: forgejo_db_name not in postgres_db_list.stdout
- name: Create Forgejo PostgreSQL user
community.postgresql.postgresql_user:
name: "{{ forgejo_db_user }}"
password: "{{ forgejo_db_password }}"
state: present
become: yes
become_user: postgres
no_log: yes
- name: Grant database privileges to Forgejo user
community.postgresql.postgresql_privs:
database: "{{ forgejo_db_name }}"
roles: "{{ forgejo_db_user }}"
type: database
privs: ALL
become: yes
become_user: postgres
- name: Grant schema privileges to Forgejo user
community.postgresql.postgresql_privs:
database: "{{ forgejo_db_name }}"
roles: "{{ forgejo_db_user }}"
type: schema
objs: public
privs: ALL
become: yes
become_user: postgres
- name: Set Forgejo user as owner of public schema
community.postgresql.postgresql_owner:
db: "{{ forgejo_db_name }}"
new_owner: "{{ forgejo_db_user }}"
obj_name: public
obj_type: schema
become: yes
become_user: postgres
- name: Configure PostgreSQL for optimal performance
ansible.builtin.lineinfile:
path: "/etc/postgresql/{{ postgres_version }}/main/postgresql.conf"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
become: yes
loop:
- { regexp: '^max_connections', line: "max_connections = {{ postgres_max_connections }}" }
- { regexp: '^shared_buffers', line: "shared_buffers = {{ postgres_shared_buffers }}" }
- { regexp: '^effective_cache_size', line: "effective_cache_size = {{ postgres_effective_cache_size }}" }
- { regexp: '^maintenance_work_mem', line: "maintenance_work_mem = 128MB" }
- { regexp: '^checkpoint_completion_target', line: "checkpoint_completion_target = 0.9" }
- { regexp: '^wal_buffers', line: "wal_buffers = 16MB" }
- { regexp: '^default_statistics_target', line: "default_statistics_target = 100" }
- { regexp: '^random_page_cost', line: "random_page_cost = 1.1" }
- { regexp: '^effective_io_concurrency', line: "effective_io_concurrency = 200" }
- { regexp: '^work_mem', line: "work_mem = 8MB" }
- { regexp: '^min_wal_size', line: "min_wal_size = 1GB" }
- { regexp: '^max_wal_size', line: "max_wal_size = 4GB" }
notify: Restart PostgreSQL
- name: Configure PostgreSQL to listen on all interfaces
ansible.builtin.lineinfile:
path: "/etc/postgresql/{{ postgres_version }}/main/postgresql.conf"
regexp: "^#?listen_addresses"
line: "listen_addresses = '*'"
state: present
become: yes
notify: Restart PostgreSQL
- name: Configure PostgreSQL authentication
ansible.builtin.lineinfile:
path: "/etc/postgresql/{{ postgres_version }}/main/pg_hba.conf"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
become: yes
loop:
- regexp: '^local\s+all\s+postgres'
line: 'local all postgres peer'
- regexp: '^local\s+all\s+all'
line: 'local all all peer'
- regexp: '^host\s+all\s+all\s+127\.0\.0\.1'
line: 'host all all 127.0.0.1/32 scram-sha-256'
- regexp: '^host\s+all\s+all\s+::1'
line: 'host all all ::1/128 scram-sha-256'
notify: Restart PostgreSQL
- name: Allow Docker network to connect to PostgreSQL
ansible.builtin.lineinfile:
path: "/etc/postgresql/{{ postgres_version }}/main/pg_hba.conf"
line: 'host all all 172.16.0.0/12 scram-sha-256'
insertafter: '^host\s+all\s+all\s+127'
state: present
become: yes
notify: Restart PostgreSQL
- name: Enable PostgreSQL extensions
community.postgresql.postgresql_ext:
name: "{{ item }}"
db: "{{ forgejo_db_name }}"
state: present
become: yes
become_user: postgres
loop:
- pg_trgm
- btree_gin
- name: Create PostgreSQL backup script
ansible.builtin.template:
src: postgres_backup.sh.j2
dest: /usr/local/bin/postgres_backup.sh
mode: '0755'
become: yes
when: forgejo_enable_backups | bool

View file

@ -0,0 +1,194 @@
---
# System preparation tasks
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
become: yes
- name: Upgrade all packages
ansible.builtin.apt:
upgrade: safe
become: yes
tags:
- upgrade
- name: Install system packages
ansible.builtin.apt:
name: "{{ system_packages }}"
state: present
become: yes
- name: Check if Forgejo group exists
ansible.builtin.getent:
database: group
key: "{{ forgejo_group }}"
register: forgejo_group_check
ignore_errors: yes
become: yes
- name: Create Forgejo system group
ansible.builtin.group:
name: "{{ forgejo_group }}"
gid: "{{ forgejo_gid }}"
system: yes
state: present
become: yes
when: forgejo_group_check.failed | default(false)
- name: Ensure Forgejo group exists (if already created with different GID)
ansible.builtin.group:
name: "{{ forgejo_group }}"
system: yes
state: present
become: yes
when: not (forgejo_group_check.failed | default(false))
- name: Check if Forgejo user exists
ansible.builtin.getent:
database: passwd
key: "{{ forgejo_user }}"
register: forgejo_user_check
ignore_errors: yes
become: yes
- name: Create Forgejo system user
ansible.builtin.user:
name: "{{ forgejo_user }}"
uid: "{{ forgejo_uid }}"
group: "{{ forgejo_group }}"
system: yes
shell: /bin/bash
home: "{{ forgejo_base_path }}"
create_home: no
state: present
become: yes
when: forgejo_user_check.failed | default(false)
- name: Ensure Forgejo user exists (if already created with different UID)
ansible.builtin.user:
name: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
system: yes
shell: /bin/bash
home: "{{ forgejo_base_path }}"
create_home: no
state: present
become: yes
when: not (forgejo_user_check.failed | default(false))
- name: Create Forgejo directory structure
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0755'
become: yes
loop:
- "{{ forgejo_base_path }}"
- "{{ forgejo_data_path }}"
- "{{ forgejo_config_path }}"
- "{{ forgejo_custom_path }}"
- "{{ forgejo_backup_path }}"
- "{{ forgejo_data_path }}/git"
- "{{ forgejo_data_path }}/attachments"
- "{{ forgejo_data_path }}/lfs"
- "{{ forgejo_data_path }}/avatars"
- name: Configure system limits for Forgejo
ansible.builtin.pam_limits:
domain: "{{ forgejo_user }}"
limit_type: "{{ item.limit_type }}"
limit_item: "{{ item.limit_item }}"
value: "{{ item.value }}"
become: yes
loop:
- { limit_type: 'soft', limit_item: 'nofile', value: '65535' }
- { limit_type: 'hard', limit_item: 'nofile', value: '65535' }
- { limit_type: 'soft', limit_item: 'nproc', value: '65535' }
- { limit_type: 'hard', limit_item: 'nproc', value: '65535' }
- name: Configure kernel parameters
ansible.builtin.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: yes
become: yes
loop:
- { name: 'net.core.somaxconn', value: '1024' }
- { name: 'net.ipv4.tcp_max_syn_backlog', value: '2048' }
- { name: 'net.ipv4.ip_forward', value: '1' }
- { name: 'vm.swappiness', value: '10' }
- { name: 'fs.file-max', value: '65535' }
# NOTE: UFW firewall configuration is handled by ufw.yml
# We only set up minimal rules here for Docker access during deployment
# The full secure configuration (Tailscale-only SSH) is applied in ufw.yml
- name: Install UFW
ansible.builtin.apt:
name: ufw
state: present
become: yes
when: ansible_os_family == "Debian"
- name: Allow Docker network to access host services
community.general.ufw:
rule: allow
from_ip: 172.16.0.0/12
comment: "Allow Docker containers to access host services (PostgreSQL, etc.)"
become: yes
when: ansible_os_family == "Debian"
- name: Set timezone to UTC
community.general.timezone:
name: UTC
become: yes
- name: Enable automatic security updates
ansible.builtin.apt:
name: unattended-upgrades
state: present
become: yes
- name: Configure unattended upgrades
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
content: |
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
mode: '0644'
become: yes
- name: Ensure SSH is properly configured
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
validate: '/usr/sbin/sshd -t -f %s'
become: yes
loop:
- { regexp: '^PermitRootLogin', line: 'PermitRootLogin prohibit-password' }
- { regexp: '^PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^PubkeyAuthentication', line: 'PubkeyAuthentication yes' }
notify: Restart sshd
when: ansible_connection != 'local'
- name: Create systemd service for sshd
ansible.builtin.systemd:
name: sshd
enabled: yes
state: started
become: yes
when: ansible_connection != 'local'

View file

@ -0,0 +1,40 @@
---
# Redis setup tasks
- name: Install Redis
ansible.builtin.apt:
name:
- redis-server
- redis-tools
state: present
update_cache: yes
become: yes
- name: Configure Redis
ansible.builtin.lineinfile:
path: /etc/redis/redis.conf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
become: yes
loop:
- { regexp: '^bind', line: 'bind 127.0.0.1 ::1' }
- { regexp: '^protected-mode', line: 'protected-mode yes' }
- { regexp: '^maxmemory', line: 'maxmemory 256mb' }
- { regexp: '^maxmemory-policy', line: 'maxmemory-policy allkeys-lru' }
- { regexp: '^save', line: 'save 900 1' }
notify: Restart Redis
- name: Ensure Redis is started and enabled
ansible.builtin.systemd:
name: redis-server
state: started
enabled: yes
become: yes
- name: Test Redis connection
ansible.builtin.command:
cmd: redis-cli ping
register: redis_ping
changed_when: false
failed_when: redis_ping.stdout != "PONG"

View file

@ -0,0 +1,29 @@
---
# SSL/TLS setup for Caddy
# Note: Caddy handles Let's Encrypt certificates automatically!
# This file only sets up log directories and verifies configuration.
- name: Create Caddy log directory
ansible.builtin.file:
path: /var/log/caddy
state: directory
owner: caddy
group: caddy
mode: '0755'
become: yes
- name: Verify Caddy is configured for HTTPS
ansible.builtin.debug:
msg: >
Caddy will automatically obtain and renew TLS certificates for {{ forgejo_domain }}
using Let's Encrypt. The email {{ letsencrypt_email }} will be used for renewal
notifications. No manual certificate management is required.
- name: Ensure Caddy data directory exists (for certificates)
ansible.builtin.file:
path: /var/lib/caddy/.local/share/caddy
state: directory
owner: caddy
group: caddy
mode: '0700'
become: yes

View file

@ -0,0 +1,76 @@
---
# Tailscale VPN installation and configuration
# Provides secure access to SSH and internal services
- name: Install prerequisites for Tailscale
ansible.builtin.apt:
name:
- curl
- gnupg
- apt-transport-https
state: present
update_cache: yes
become: yes
- name: Add Tailscale GPG key
ansible.builtin.shell: |
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
args:
creates: /usr/share/keyrings/tailscale-archive-keyring.gpg
become: yes
- name: Add Tailscale repository
ansible.builtin.shell: |
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list > /dev/null
args:
creates: /etc/apt/sources.list.d/tailscale.list
become: yes
- name: Install Tailscale
ansible.builtin.apt:
name: tailscale
state: present
update_cache: yes
become: yes
- name: Enable Tailscale service
ansible.builtin.systemd:
name: tailscaled
state: started
enabled: yes
become: yes
- name: Check if Tailscale is already authenticated
ansible.builtin.command: tailscale status
register: tailscale_status
ignore_errors: yes
changed_when: false
become: yes
- name: Display Tailscale authentication instructions
ansible.builtin.debug:
msg: |
===============================================================
TAILSCALE AUTHENTICATION REQUIRED
===============================================================
Tailscale is installed but needs to be authenticated.
SSH into the server and run:
sudo tailscale up --ssh
This will:
1. Open a browser URL for authentication
2. Connect to your Tailnet
3. Enable Tailscale SSH (optional but recommended)
For headless servers, use an auth key:
sudo tailscale up --authkey=tskey-auth-XXXXX
Generate an auth key at: https://login.tailscale.com/admin/settings/keys
After authentication, you can access this server via:
- Tailscale IP (shown in 'tailscale ip')
- Tailscale hostname (from admin console)
===============================================================
when: tailscale_status.rc != 0

View file

@ -0,0 +1,142 @@
---
# UFW Firewall configuration for Forgejo
# Restricts SSH access to Tailscale interface only
# Only exposes HTTP/HTTPS to the public internet
- name: Install UFW
ansible.builtin.apt:
name: ufw
state: present
update_cache: yes
become: yes
tags:
- install
- ufw
- name: Deploy Forgejo UFW application profile
ansible.builtin.template:
src: ufw-forgejo.j2
dest: /etc/ufw/applications.d/forgejo
owner: root
group: root
mode: '0644'
become: yes
tags:
- configure
- ufw
- name: Reset UFW to default (clean slate)
community.general.ufw:
state: reset
become: yes
tags:
- configure
- ufw
- name: Set default incoming policy to deny
community.general.ufw:
direction: incoming
policy: deny
become: yes
tags:
- configure
- ufw
- name: Set default outgoing policy to allow
community.general.ufw:
direction: outgoing
policy: allow
become: yes
tags:
- configure
- ufw
- name: Allow all traffic on Tailscale interface
community.general.ufw:
rule: allow
interface: "{{ tailscale_interface }}"
direction: in
comment: "Allow all Tailscale traffic (SSH, monitoring, internal services)"
become: yes
tags:
- configure
- ufw
- name: Allow Docker network to access host services
community.general.ufw:
rule: allow
from_ip: 172.16.0.0/12
comment: "Allow Docker containers to access host services (PostgreSQL, etc.)"
become: yes
tags:
- configure
- ufw
# Public-facing ports (Caddy handles HTTPS)
- name: Allow HTTP (Caddy)
community.general.ufw:
rule: allow
port: "80"
proto: tcp
comment: "HTTP - Caddy (redirects to HTTPS)"
become: yes
tags:
- configure
- ufw
- name: Allow HTTPS (Caddy)
community.general.ufw:
rule: allow
port: "443"
proto: tcp
comment: "HTTPS - Caddy/Forgejo"
become: yes
tags:
- configure
- ufw
# Git SSH is only accessible via Tailscale (through the interface rule above)
# Regular SSH is only accessible via Tailscale (through the interface rule above)
- name: Enable UFW logging
community.general.ufw:
logging: "on"
become: yes
tags:
- configure
- ufw
- name: Enable UFW
community.general.ufw:
state: enabled
become: yes
tags:
- configure
- ufw
- name: Display UFW security configuration
ansible.builtin.debug:
msg: |
===============================================================
FIREWALL CONFIGURED - SECURITY SUMMARY
===============================================================
PUBLIC ACCESS (from anywhere):
- Port 80/tcp (HTTP - redirects to HTTPS)
- Port 443/tcp (HTTPS - Forgejo web interface)
TAILSCALE-ONLY ACCESS (via {{ tailscale_interface }}):
- Port 22/tcp (SSH - system administration)
- Port 2222/tcp (Git SSH - clone/push/pull)
- Port 3000/tcp (Forgejo internal - for debugging)
- Port 9090/tcp (Prometheus - if enabled)
- All other internal services
Git clone URLs:
- HTTPS (public): https://{{ forgejo_domain }}/user/repo.git
- SSH (Tailscale): git@<tailscale-hostname>:user/repo.git
To access SSH after this change:
ssh root@<tailscale-ip-or-hostname>
===============================================================

View file

@ -0,0 +1,60 @@
---
# External volume setup tasks
- name: Check if volume device exists
ansible.builtin.stat:
path: "{{ forgejo_volume_device }}"
register: volume_device
- name: Fail if volume device not found
ansible.builtin.fail:
msg: "Volume device {{ forgejo_volume_device }} not found"
when: not volume_device.stat.exists
- name: Check if volume is already formatted
ansible.builtin.command:
cmd: "blkid {{ forgejo_volume_device }}"
register: volume_formatted
changed_when: false
failed_when: false
- name: Format volume with ext4
ansible.builtin.filesystem:
fstype: ext4
dev: "{{ forgejo_volume_device }}"
become: yes
when: volume_formatted.rc != 0
- name: Create mount point
ansible.builtin.file:
path: "{{ forgejo_volume_mount }}"
state: directory
mode: '0755'
become: yes
- name: Mount volume
ansible.posix.mount:
path: "{{ forgejo_volume_mount }}"
src: "{{ forgejo_volume_device }}"
fstype: ext4
opts: defaults,nofail
state: mounted
become: yes
- name: Update data path to use volume
ansible.builtin.set_fact:
forgejo_data_path: "{{ forgejo_volume_mount }}/data"
- name: Create data directories on volume
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ forgejo_user }}"
group: "{{ forgejo_group }}"
mode: '0755'
become: yes
loop:
- "{{ forgejo_data_path }}"
- "{{ forgejo_data_path }}/git"
- "{{ forgejo_data_path }}/attachments"
- "{{ forgejo_data_path }}/lfs"

View file

@ -0,0 +1,63 @@
# Caddyfile for Forgejo
# Caddy automatically obtains and renews TLS certificates via Let's Encrypt
{% if forgejo_enable_letsencrypt %}
{{ forgejo_domain }} {
# Reverse proxy to Forgejo
reverse_proxy localhost:{{ forgejo_http_port }} {
# WebSocket support (needed for real-time features)
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
# Timeouts for large Git operations
transport http {
read_timeout 600s
write_timeout 600s
}
}
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
}
# Request body size for large uploads (Git push, LFS)
request_body {
max_size 100MB
}
# Logging
log {
output file /var/log/caddy/forgejo_access.log {
roll_size 100mb
roll_keep 5
}
format json
}
# TLS configuration (automatic via Let's Encrypt)
tls {{ letsencrypt_email }}
}
{% else %}
# HTTP-only configuration (not recommended for production)
:80 {
reverse_proxy localhost:{{ forgejo_http_port }} {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
request_body {
max_size 100MB
}
log {
output file /var/log/caddy/forgejo_access.log
format json
}
}
{% endif %}

View file

@ -0,0 +1,219 @@
; Forgejo Configuration File
; Generated by Ansible
APP_NAME = Forgejo: {{ forgejo_domain }}
RUN_MODE = prod
RUN_USER = {{ forgejo_user }}
WORK_PATH = /data/gitea
[repository]
ROOT = /data/git/repositories
SCRIPT_TYPE = bash
DEFAULT_BRANCH = main
DEFAULT_PRIVATE = last
MAX_CREATION_LIMIT = -1
ENABLE_PUSH_CREATE_USER = true
ENABLE_PUSH_CREATE_ORG = true
DISABLE_HTTP_GIT = {{ forgejo_disable_http_git | lower }}
[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
[repository.upload]
ENABLED = true
TEMP_PATH = /data/gitea/uploads
FILE_MAX_SIZE = 100
MAX_FILES = 10
[lfs]
ENABLED = {{ forgejo_enable_lfs | lower }}
PATH = /data/lfs
MAX_FILE_SIZE = {{ forgejo_lfs_max_file_size }}
[server]
; Forgejo listens on HTTP internally; Caddy handles TLS termination
PROTOCOL = http
DOMAIN = {{ forgejo_domain }}
ROOT_URL = {{ forgejo_protocol }}://{{ forgejo_domain }}/
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3000
DISABLE_SSH = false
SSH_DOMAIN = {{ forgejo_domain }}
SSH_PORT = {{ forgejo_ssh_port }}
SSH_LISTEN_PORT = 22
OFFLINE_MODE = false
APP_DATA_PATH = /data/gitea
LANDING_PAGE = explore
LFS_START_SERVER = {{ forgejo_enable_lfs | lower }}
[database]
DB_TYPE = {{ forgejo_db_type }}
; Use host.docker.internal to reach host PostgreSQL from container
HOST = host.docker.internal:{{ forgejo_db_port }}
NAME = {{ forgejo_db_name }}
USER = {{ forgejo_db_user }}
PASSWD = {{ forgejo_db_password }}
SCHEMA =
SSL_MODE = disable
CHARSET = utf8mb4
LOG_SQL = false
MAX_IDLE_CONNS = 30
MAX_OPEN_CONNS = 100
CONN_MAX_LIFETIME = 3600
[security]
INSTALL_LOCK = true
SECRET_KEY = {{ vault_forgejo_secret_key | default('') }}
INTERNAL_TOKEN = {{ vault_forgejo_internal_token | default('') }}
PASSWORD_COMPLEXITY = lower,upper,digit,spec
MIN_PASSWORD_LENGTH = 10
PASSWORD_HASH_ALGO = argon2
[service]
DISABLE_REGISTRATION = {{ forgejo_disable_registration | lower }}
REQUIRE_SIGNIN_VIEW = {{ forgejo_require_signin_view | lower }}
REGISTER_EMAIL_CONFIRM = {{ forgejo_enable_email | lower }}
ENABLE_NOTIFY_MAIL = {{ forgejo_enable_email | lower }}
DEFAULT_KEEP_EMAIL_PRIVATE = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ORG_VISIBILITY = private
ENABLE_CAPTCHA = true
ENABLE_TIMETRACKING = true
DEFAULT_ENABLE_TIMETRACKING = true
ENABLE_USER_HEATMAP = true
[service.explore]
REQUIRE_SIGNIN_VIEW = {{ forgejo_require_signin_view | lower }}
DISABLE_USERS_PAGE = false
{% if forgejo_enable_email %}
[mailer]
ENABLED = true
SMTP_ADDR = {{ forgejo_email_host }}
SMTP_PORT = {{ forgejo_email_port }}
FROM = {{ forgejo_email_from }}
USER = {{ forgejo_email_user }}
PASSWD = {{ forgejo_email_password }}
SUBJECT_PREFIX = [{{ forgejo_domain }}]
MAILER_TYPE = smtp
IS_TLS_ENABLED = true
{% endif %}
[session]
PROVIDER = file
PROVIDER_CONFIG = /data/gitea/sessions
COOKIE_SECURE = {{ (forgejo_protocol == 'https') | lower }}
COOKIE_NAME = i_like_forgejo
COOKIE_DOMAIN = {{ forgejo_domain }}
GC_INTERVAL_TIME = 86400
SESSION_LIFE_TIME = 86400
[picture]
DISABLE_GRAVATAR = {{ forgejo_disable_gravatar | lower }}
ENABLE_FEDERATED_AVATAR = false
[attachment]
ENABLED = true
PATH = /data/attachments
MAX_SIZE = 100
MAX_FILES = 10
[time]
DEFAULT_UI_LOCATION = UTC
[log]
MODE = console, file
LEVEL = {{ forgejo_log_level }}
ROOT_PATH = /data/gitea/log
ENABLE_XORM_LOG = false
[log.console]
LEVEL = {{ forgejo_log_level }}
COLORIZE = false
[log.file]
LEVEL = {{ forgejo_log_level }}
FILE_NAME = forgejo.log
MAX_SIZE_SHIFT = 28
DAILY_ROTATE = true
MAX_DAYS = 7
[git]
MAX_GIT_DIFF_LINES = 1000
MAX_GIT_DIFF_LINE_CHARACTERS = 5000
MAX_GIT_DIFF_FILES = 100
GC_ARGS =
[git.timeout]
DEFAULT = 360
MIGRATE = 600
MIRROR = 300
CLONE = 300
PULL = 300
GC = 60
{% if forgejo_enable_2fa %}
[two_factor]
ENABLED = true
{% endif %}
[openid]
ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false
[cron]
ENABLED = true
RUN_AT_START = false
[cron.update_mirrors]
SCHEDULE = @every 10m
[cron.repo_health_check]
SCHEDULE = @every 24h
TIMEOUT = 60s
[cron.check_repo_stats]
SCHEDULE = @every 24h
[cron.cleanup_hook_task_table]
SCHEDULE = @every 24h
CLEANUP_TYPE = OlderThan
OLDER_THAN = 168h
[cron.update_migration_poster_id]
SCHEDULE = @every 24h
[cron.sync_external_users]
SCHEDULE = @every 24h
UPDATE_EXISTING = true
[api]
ENABLE_SWAGGER = false
MAX_RESPONSE_ITEMS = 50
DEFAULT_PAGING_NUM = 30
DEFAULT_GIT_TREES_PER_PAGE = 1000
DEFAULT_MAX_BLOB_SIZE = 10485760
[oauth2]
ENABLED = true
JWT_SECRET = {{ vault_forgejo_jwt_secret | default('') }}
[webhook]
QUEUE_LENGTH = 1000
DELIVER_TIMEOUT = 15
SKIP_TLS_VERIFY = false
PAGING_NUM = 10
[metrics]
ENABLED = {{ forgejo_enable_prometheus | lower }}
TOKEN = {{ vault_forgejo_metrics_token | default('') }}
[task]
QUEUE_TYPE = channel
QUEUE_LENGTH = 10000
QUEUE_CONN_STR =
QUEUE_BATCH_NUMBER = 20
[indexer]
ISSUE_INDEXER_TYPE = db
REPO_INDEXER_ENABLED = true

View file

@ -0,0 +1,27 @@
# Docker Compose override for Prometheus monitoring
# Generated by Ansible
# This file extends the main docker-compose.yml
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle'
ports:
# Only bind to localhost for security - not exposed externally
- "127.0.0.1:{{ prometheus_port | default(9090) }}:9090"
networks:
- forgejo-network
networks:
forgejo-network:
external: true
name: {{ forgejo_base_path | basename }}_default

View file

@ -0,0 +1,76 @@
services:
forgejo:
image: {{ forgejo_docker_image }}:{{ forgejo_version }}
container_name: forgejo
restart: unless-stopped
environment:
- USER_UID={{ forgejo_uid }}
- USER_GID={{ forgejo_gid }}
- FORGEJO__database__DB_TYPE={{ forgejo_db_type }}
- FORGEJO__database__HOST=host.docker.internal:{{ forgejo_db_port }}
- FORGEJO__database__NAME={{ forgejo_db_name }}
- FORGEJO__database__USER={{ forgejo_db_user }}
- FORGEJO__database__PASSWD={{ forgejo_db_password }}
{% if forgejo_use_redis %}
- FORGEJO__cache__ENABLED=true
- FORGEJO__cache__ADAPTER=redis
- FORGEJO__cache__HOST=redis://redis:{{ redis_port }}/0
- FORGEJO__session__PROVIDER=redis
- FORGEJO__session__PROVIDER_CONFIG=redis://redis:{{ redis_port }}/0
{% endif %}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- {{ forgejo_data_path }}/git:/data/git
- {{ forgejo_data_path }}/attachments:/data/attachments
- {{ forgejo_data_path }}/lfs:/data/lfs
- {{ forgejo_config_path }}/app.ini:/data/gitea/conf/app.ini
- {{ forgejo_custom_path }}:/data/gitea/custom
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:{{ forgejo_http_port }}:3000"
- "{{ forgejo_ssh_port }}:22"
networks:
- forgejo
{% if forgejo_use_redis %}
depends_on:
- redis
{% endif %}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
{% if forgejo_use_redis %}
redis:
image: redis:7-alpine
container_name: forgejo-redis
restart: unless-stopped
volumes:
- {{ forgejo_data_path }}/redis:/data
networks:
- forgejo
command: redis-server --appendonly yes
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "3"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 3s
retries: 3
{% endif %}
networks:
forgejo:
driver: bridge

View file

@ -0,0 +1,19 @@
[Unit]
Description=Forgejo Git Server (Docker Compose)
Documentation=https://forgejo.org/docs/latest/
After=docker.service
Requires=docker.service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory={{ forgejo_base_path }}
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
ExecReload=/usr/bin/docker compose restart
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,33 @@
#!/bin/bash
# Forgejo Backup Script
# Generated by Ansible
set -e
BACKUP_DIR="{{ forgejo_backup_path }}"
TIMESTAMP=$(date +%Y%m%dT%H%M%S)
LOG_FILE="/var/log/forgejo-backup.log"
echo "[$(date)] Starting Forgejo backup..." | tee -a "$LOG_FILE"
# Create database backup
pg_dump -U {{ forgejo_db_user }} {{ forgejo_db_name }} | gzip > "$BACKUP_DIR/database-$TIMESTAMP.sql.gz"
echo "[$(date)] Database backed up" | tee -a "$LOG_FILE"
# Backup repositories
tar -czf "$BACKUP_DIR/repositories-$TIMESTAMP.tar.gz" -C {{ forgejo_data_path }} git
echo "[$(date)] Repositories backed up" | tee -a "$LOG_FILE"
# Backup configuration
tar -czf "$BACKUP_DIR/config-$TIMESTAMP.tar.gz" {{ forgejo_config_path }} {{ forgejo_base_path }}/docker-compose.yml
echo "[$(date)] Configuration backed up" | tee -a "$LOG_FILE"
# Backup data
tar -czf "$BACKUP_DIR/data-$TIMESTAMP.tar.gz" -C {{ forgejo_data_path }} attachments lfs avatars
echo "[$(date)] Data backed up" | tee -a "$LOG_FILE"
# Clean old backups
find "$BACKUP_DIR" -type f -name "*.gz" -mtime +{{ forgejo_backup_retention_days }} -delete
echo "[$(date)] Old backups cleaned" | tee -a "$LOG_FILE"
echo "[$(date)] Backup completed successfully" | tee -a "$LOG_FILE"

View file

@ -0,0 +1,24 @@
#!/bin/bash
# PostgreSQL Backup Script
# Generated by Ansible
set -e
BACKUP_DIR="{{ forgejo_backup_path }}"
TIMESTAMP=$(date +%Y%m%dT%H%M%S)
LOG_FILE="/var/log/postgres-backup.log"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
echo "[$(date)] Starting PostgreSQL backup..." | tee -a "$LOG_FILE"
# Create database backup
sudo -u postgres pg_dump {{ forgejo_db_name }} | gzip > "$BACKUP_DIR/postgres-$TIMESTAMP.sql.gz"
echo "[$(date)] PostgreSQL backup completed: postgres-$TIMESTAMP.sql.gz" | tee -a "$LOG_FILE"
# Clean old PostgreSQL backups (keep last {{ forgejo_backup_retention_days }} days)
find "$BACKUP_DIR" -type f -name "postgres-*.sql.gz" -mtime +{{ forgejo_backup_retention_days }} -delete
echo "[$(date)] Old PostgreSQL backups cleaned" | tee -a "$LOG_FILE"

View file

@ -0,0 +1,42 @@
# Prometheus configuration for Forgejo monitoring
# Generated by Ansible
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Forgejo metrics endpoint
- job_name: 'forgejo'
scheme: http
static_configs:
- targets: ['forgejo:3000']
metrics_path: /metrics
bearer_token: '{{ vault_forgejo_metrics_token | default("") }}'
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
{% if forgejo_db_type == 'postgres' %}
# PostgreSQL metrics (if postgres_exporter is enabled)
# Uncomment and configure if you add postgres_exporter
# - job_name: 'postgres'
# static_configs:
# - targets: ['postgres_exporter:9187']
{% endif %}
{% if forgejo_use_redis %}
# Redis metrics (if redis_exporter is enabled)
# Uncomment and configure if you add redis_exporter
# - job_name: 'redis'
# static_configs:
# - targets: ['redis_exporter:9121']
{% endif %}
# Node metrics (if node_exporter is enabled)
# Uncomment and configure if you add node_exporter
# - job_name: 'node'
# static_configs:
# - targets: ['node_exporter:9100']

View file

@ -0,0 +1,14 @@
[Forgejo]
title=Forgejo Git Forge
description=Forgejo self-hosted Git service (web interface)
ports=80,443/tcp
[Forgejo-SSH]
title=Forgejo Git SSH
description=Forgejo Git operations over SSH
ports={{ forgejo_ssh_port }}/tcp
[Forgejo-Full]
title=Forgejo Full
description=Forgejo web interface and Git SSH
ports=80,443,{{ forgejo_ssh_port }}/tcp