Deploying Gitea with Ansible

November 10, 2020

If you want to host a simple and lightweight git server along with a pretty web interface, Gitea is the way to go. After setting up blog deployment via Ansible, I set out to write a deployment for a personal Gitea server.

All .yaml files of the final deployment role can be found at the end of this page.

Since Gitea is contained within a single binary, deployment for the server itself will be as follows:

We will also set up a dedicated user to run the server.

Binary installation

To start things off, we will clone the binary from the GitHub repository. We set two variables with the download URL and a SHA256 checksum:

# roles/gitea/defaults/main.yml
gitea_binary_download_url: https://github.com/go-gitea/gitea/releases/download/v1.12.5/gitea-1.12.5-linux-amd64
gitea_binary_checksum: sha256:8ed8bff1f34d8012cab92943214701c10764ffaca102e311a3297edbb8fce940

The builtin get_url module will then manage installing the binary:

# roles/gitea/tasks/main.yml
- name: install the binary
  get_url:
    url: "{{ gitea_binary_download_url }}"
    checksum: "{{ gitea_binary_checksum }}"
    dest: /usr/local/bin/gitea
    owner: root
    group: root
    mode: 0555
  notify:
    - restart the gitea service

Note that we hook up a handler to restart the Gitea service in case the binary changed. The handler definition simply uses the systemd service we are about to define:

# roles/gitea/handlers/main.yml
- name: restart the gitea service
  service:
    name: gitea.service
    state: restarted

Service user and configuration directory

Before we configure the systemd service, we set up a dedicated system user to run the service and store its data. The home directory is configured into the standard location for service state, which conveniently saves us from setting it up separately. A configuration directory for gitea itself is also created:

# roles/gitea/tasks/main.yml
- name: create the service user
  user:
    name: gitea
    state: present
    system: true
    home: /var/lib/gitea

- name: create the configuration directory
  file:
    path: /etc/gitea
    state: directory
    owner: gitea
    group: root
    mode: 0500

Note that we grant the gitea user write permissions on the configuration directory. This is intentional, as Gitea will write the configuration file on initial startup, even if already properly configured.

Server configuration

Our template task will use two rarely used options, variable_start_string and variable_end_string, in order to set different templating tokens. We need to add these, as Gitea uses {{ and }} itself, which would otherwise conflict with the module:

# roles/gitea/tasks/main.yml
- name: template the configuration
  template:
    src: app.ini.j2
    dest: /etc/gitea/app.ini
    owner: gitea
    group: root
    # gitea needs rw permissions to this on first launch
    mode: 0600
    variable_start_string: ((
    variable_end_string: ))
  notify:
    - restart the gitea service

To retrieve a sample configuration file, I started the service and copied the configuration file that was written. If you take the example configuration file from the documentation, it is likely that Ansible will make changes every time you deployment, ruining your diffs. That’s inconvenient!

For brevity I will not include the entire configuration file here. I’ve added the following variables, which contain both the sections & option names of the configuration file, making them easy to find:

# result of ``:g/((  `` on ``roles/gitea/templates/app.ini.j2``
APP_NAME = (( gitea_cfg_app_name ))
DOMAIN                          = (( gitea_cfg_server_domain ))
HTTP_ADDR                       = (( gitea_cfg_server_http_addr ))
START_SSH_SERVER                = (( gitea_cfg_server_start_ssh_server ))
SSH_PORT                        = (( gitea_cfg_server_ssh_port ))
SSH_LISTEN_PORT                 = (( gitea_cfg_server_ssh_listen_port ))
SECRET_KEY                               = (( gitea_cfg_security_secret_key ))
INTERNAL_TOKEN                           = (( gitea_cfg_security_internal_token ))

I recommend that you keep gitea_cfg_security_secret_key and gitea_cfg_security_internal_token in Ansible Vault.

Systemd service configuration

Now that we have configured the Gitea server itself, we need to configure the systemd service. This is accomplished with another template task:

# roles/gitea/defaults/main.yml
- name: template the service file
  template:
    src: gitea.service.j2
    dest: /etc/systemd/system/gitea.service
    owner: root
    group: root
  register: gitea_service_file_task
  notify:
    - restart the gitea service

The service file is, at its core, rather simple:

# roles/gitea/templates/gitea.service.j2

[Unit]
Description=self-hosted git service
Documentation=https://docs.gitea.io/en-us/

[Service]
User=gitea
Group=gitea
RuntimeDirectory=gitea
StateDirectory=gitea
StateDirectoryMode=0700
ExecStart=/usr/local/bin/gitea -c /etc/gitea/app.ini
WorkingDirectory=/var/lib/gitea

[Install]
WantedBy=network-online.target

My service file has a bit more content in order to sandbox the service. This is not required, but I believe it is good practice to use systemd’s sandboxing capabilities. You can find the complete options at the end of this post.

Note that we register the task output to reuse it in the upcoming service start task:

# roles/gitea/defaults/main.yml
- name: enable and start gitea
  service:
    name: gitea.service
    state: started
    enabled: true
    daemon_reload: "{{ gitea_service_file_task is changed }}"

That wraps up the content of the gitea role. Set variables, deploy it on your server of choice and enjoy your self-hosted git server! Feel free to send me an e-mail if you have any questions.

Further steps

My deployment goes a bit further than just deploying the Gitea binary. I use my nginx-letsencrypt role from Automatically secure NGINX with Let’s Encrypt and Ansible in order to first retrieve SSL certificates, then another role called gitea-nginx to configure the Gitea virtual host in nginx and allow SSH traffic through the firewall.

Complete role contents

As promised, here are the complete contents of each file in my role:

roles/gitea/handlers/main.yml
---
- name: restart the gitea service
  service:
    name: gitea.service
    state: restarted
roles/gitea/defaults/main.yml
---
## binary download options
gitea_binary_download_url: https://github.com/go-gitea/gitea/releases/download/v1.12.5/gitea-1.12.5-linux-amd64
gitea_binary_checksum: sha256:8ed8bff1f34d8012cab92943214701c10764ffaca102e311a3297edbb8fce940

## service configuration
# resource control
gitea_svc_cpu_quota: 50%
gitea_svc_memory_low: 75M
gitea_svc_memory_high: 230M
gitea_svc_memory_max: 250M
gitea_svc_tasks_max: 50

## gitea configuration file
# top-level options
gitea_cfg_app_name: 'jc : gitea'

# [server]
gitea_cfg_server_http_addr: 127.0.0.1
gitea_cfg_server_domain: git.(( ansible_fqdn ))
gitea_cfg_server_start_ssh_server: false
gitea_cfg_server_ssh_port: 22
gitea_cfg_server_ssh_listen_port: "(( gitea_cfg_server_ssh_port ))"

# [security]
gitea_cfg_security_secret_key: "(( lookup('password', 'secrets/' + ansible_nodename + '/gitea/secret_key') ))"
roles/gitea/tasks/main.yml
---
# This task is included here for completeness, but I expect
# ``git`` to already # be present on most systems.
- name: install git
  package:
    name: git-core
    state: present

- name: install the binary
  get_url:
    url: "{{ gitea_binary_download_url }}"
    checksum: "{{ gitea_binary_checksum }}"
    dest: /usr/local/bin/gitea
    owner: root
    group: root
    mode: 0555
  notify:
    - restart the gitea service

- name: template the service file
  template:
    src: gitea.service.j2
    dest: /etc/systemd/system/gitea.service
    owner: root
    group: root
  register: gitea_service_file_task
  notify:
    - restart the gitea service

- name: create the service user
  user:
    name: gitea
    state: present
    system: true
    home: /var/lib/gitea

- name: create the configuration directory
  file:
    path: /etc/gitea
    state: directory
    owner: gitea
    group: root
    mode: 0500

- name: template the configuration
  template:
    src: app.ini.j2
    dest: /etc/gitea/app.ini
    owner: gitea
    group: root
    # gitea needs rw permissions to this on first launch
    mode: 0600
    variable_start_string: ((
    variable_end_string: ))
  notify:
    - restart the gitea service

- name: enable and start gitea
  service:
    name: gitea.service
    state: started
    enabled: true
    daemon_reload: "{{ gitea_service_file_task is changed }}"

# vim: sw=2 ts=2:
roles/gitea/templates/gitea.service.j2
# {{ ansible_managed }}

[Unit]
Description=self-hosted git service
Documentation=https://docs.gitea.io/en-us/

[Service]
User=gitea
Group=gitea
RuntimeDirectory=gitea
StateDirectory=gitea
StateDirectoryMode=0700
ExecStart=/usr/local/bin/gitea -c /etc/gitea/app.ini
WorkingDirectory=/var/lib/gitea

# Comment out the following if your Gitea does not need external internet access
# and the only clients connecting are from localhost (such as your reverse proxy).
# The following features may not work properly when using these settings:
#   - Repository mirroring
#   - External authorization providers
# IPAddressAllow=localhost
# IPAddressDeny=any

CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
RemoveIPC=true
RestrictAddressFamilies=AF_INET
RestrictNamespaces=true
RestrictRealtime=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=[email protected] @resources
UMask=0077


# REVIEW: only needs access to /etc/gitea. use tmpfs for /etc, bind mount over
#   systemd-run -t -p TemporaryFileSystem=/etc:ro -p BindReadOnlyPaths=/etc/gitea/ bash
ReadWritePaths=/etc/gitea/app.ini
ReadWritePaths=/var/lib/gitea

# Resource control.
CPUAccounting=true
CPUQuota={{ gitea_svc_cpu_quota }}
MemoryAccounting=true
MemoryLow={{ gitea_svc_memory_low }}
MemoryHigh={{ gitea_svc_memory_high }}
MemoryMax={{ gitea_svc_memory_max }}
TasksAccounting=true
TasksMax={{ gitea_svc_tasks_max }}

[Install]
WantedBy=network-online.target

# vim: ft=dosini.jinja2:

Sorry, I did not include app.ini.j2 here, since it changes over the course of releases, and is also about the size of my entire website.