Setup a Self-Hosted Git Service with Gitea

Setup a Self-Hosted Git Service with Gitea

Gitea is a self-hosted git service written in Go, it's super lightweight to run and supports ARM architectures as well, so you can run it on a Raspberry Pi as well.

What are we doing today

In this tutorial we will be setting up a self hosted version control repository with Gitea on Docker using Traefik as our Load Balancer and SSL terminations for LetsEncrypt certificates.

We will then create a example git repository, add our ssh key to our account and clone our repository using ssh, change some code, commit and push to our repository.

Assumptions

I will assume that you have docker and docker-compose installed. If you need more info on Traefik you can have a look at their website, but I have also written a post on setting up Traefik v2 in detail, but we will touch on that in this post.

Environment Details

I have 1 DNS entry set to the following:

  • Traefik: traefik.rbkr.xyz
  • Gitea: git.rbkr.xyz

Accessing our service will be done over HTTPS on port 443, and for cloning over SSH, the port will be set to 222

Directory Structure

Create the gitea directory which will be our docker compose project directory:

mkdir gitea
cd gitea

Create the traefik directory for acme.json where certificate data will be stored, create the file and change permissions on the file:

touch traefik/acme.json
chmod 600 traefik/acme.json

Traefik

Open the docker-compose.yml and add the first bit which will Traefik, ensure that you replace the following:

  • me@example.com with your email under certificatesResolvers.letsencrypt.acme.email
  • traefik.rbkr.xyz with your fqdn for traefik under traefik.http.routers.api.rule=Host()

The section for traefik:

version: '3.8'

services:
  gitea-traefik:
    image: traefik:2.4
    container_name: gitea-traefik
    restart: unless-stopped
    volumes:
      - ./traefik/acme.json:/acme.json
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - public
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.api.rule=Host(`traefik.rbkr.xyz`)'
      - 'traefik.http.routers.api.entrypoints=https'
      - 'traefik.http.routers.api.service=api@internal'
      - 'traefik.http.routers.api.tls=true'
      - 'traefik.http.routers.api.tls.certresolver=letsencrypt'
    ports:
      - 80:80
      - 443:443
    command:
      - '--api'
      - '--providers.docker=true'
      - '--providers.docker.exposedByDefault=false'
      - '--entrypoints.http=true'
      - '--entrypoints.http.address=:80'
      - '--entrypoints.http.http.redirections.entrypoint.to=https'
      - '--entrypoints.http.http.redirections.entrypoint.scheme=https'
      - '--entrypoints.https=true'
      - '--entrypoints.https.address=:443'
      - '--certificatesResolvers.letsencrypt.acme.email=me@example.com`'
      - '--certificatesResolvers.letsencrypt.acme.storage=acme.json'
      - '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
      - '--log=true'
      - '--log.level=INFO'
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

networks:
  public:
    name: public

We can start traefik so long:

docker-compose up -d

Gitea

Now we will add the Gitea components, I have opted in for the gitea service and a redis cache and will be making use of sqlite as this is just a demonstration. For non-test environments, you can have a look at MySQL or Postres:

Review the following configuration options:

  • DOMAIN and SSH_DOMAIN (this will be used in your clone urls)
  • ROOT_URL (this is set to use the HTTPS protocol, including my domain)
  • SSH_LISTEN_PORT (this is the port listening for SSH inside the container)
  • SSH_PORT (this is the port we are exposing from outside, which will be replaced in the clone url)
  • DB_TYPE (Im using sqlite for this example)
  • traefik.http.routers.gitea.rule=Host() (the host header to reach gitea via web)
  • ./data/gitea (I am persisting the data in my local working directory under the given path)

The gitea portion of the docker-compose.yml:

---
version: '3.8'

services:
  ...
  gitea:
    container_name: gitea
    image: gitea/gitea:${GITEA_VERSION:-1.14.5}
    restart: unless-stopped
    depends_on:
      gitea-traefik:
        condition: service_started
      gitea-cache:
        condition: service_healthy
    environment:
      - APP_NAME="Gitea"
      - USER_UID=1000
      - USER_GID=1000
      - USER=git
      - RUN_MODE=prod
      - DOMAIN=git.rbkr.xyz
      - SSH_DOMAIN=git.rbkr.xyz
      - HTTP_PORT=3000
      - ROOT_URL=https://git.rbkr.xyz
      - SSH_PORT=222
      - SSH_LISTEN_PORT=22
      - DB_TYPE=sqlite3
      - GITEA__cache__ENABLED=true
      - GITEA__cache__ADAPTER=redis
      - GITEA__cache__HOST=redis://gitea-cache:6379/0?pool_size=100&idle_timeout=180s
      - GITEA__cache__ITEM_TTL=24h
    ports:
      - "222:22"
    networks:
      - public
    volumes:
      - ./data/gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gitea.rule=Host(`git.rbkr.xyz`)"
      - "traefik.http.routers.gitea.entrypoints=https"
      - "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
      - "traefik.http.routers.gitea.service=gitea-service"
      - "traefik.http.services.gitea-service.loadbalancer.server.port=3000"
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

  gitea-cache:
    container_name: gitea-cache
    image: redis:6-alpine
    restart: unless-stopped
    networks:
      - public
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 15s
      timeout: 3s
      retries: 30
    logging:
      driver: "json-file"
      options:
        max-size: "1m"
...

So my complete docker-compose.yml will look like the following:

---
version: '3.8'

services:
  gitea-traefik:
    image: traefik:2.4
    container_name: gitea-traefik
    restart: unless-stopped
    volumes:
      - ./traefik/acme.json:/acme.json
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - public
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.api.rule=Host(`traefik.rbkr.xyz`)'
      - 'traefik.http.routers.api.entrypoints=https'
      - 'traefik.http.routers.api.service=api@internal'
      - 'traefik.http.routers.api.tls=true'
      - 'traefik.http.routers.api.tls.certresolver=letsencrypt'
    ports:
      - 80:80
      - 443:443
    command:
      - '--api'
      - '--providers.docker=true'
      - '--providers.docker.exposedByDefault=false'
      - '--entrypoints.http=true'
      - '--entrypoints.http.address=:80'
      - '--entrypoints.http.http.redirections.entrypoint.to=https'
      - '--entrypoints.http.http.redirections.entrypoint.scheme=https'
      - '--entrypoints.https=true'
      - '--entrypoints.https.address=:443'
      - '--certificatesResolvers.letsencrypt.acme.email=me@example.com'
      - '--certificatesResolvers.letsencrypt.acme.storage=acme.json'
      - '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
      - '--log=true'
      - '--log.level=INFO'
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

  gitea:
    container_name: gitea
    image: gitea/gitea:${GITEA_VERSION:-1.14.5}
    restart: unless-stopped
    depends_on:
      gitea-traefik:
        condition: service_started
      gitea-cache:
        condition: service_healthy
    environment:
      - APP_NAME="Gitea"
      - USER_UID=1000
      - USER_GID=1000
      - USER=git
      - RUN_MODE=prod
      - DOMAIN=git.rbkr.xyz
      - SSH_DOMAIN=git.rbkr.xyz
      - HTTP_PORT=3000
      - ROOT_URL=https://git.rbkr.xyz
      - SSH_PORT=222
      - SSH_LISTEN_PORT=22
      - DB_TYPE=sqlite3
      - GITEA__cache__ENABLED=true
      - GITEA__cache__ADAPTER=redis
      - GITEA__cache__HOST=redis://gitea-cache:6379/0?pool_size=100&idle_timeout=180s
      - GITEA__cache__ITEM_TTL=24h
    ports:
      - "222:22"
    networks:
      - public
    volumes:
      - ./data/gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gitea.rule=Host(`git.rbkr.xyz`)"
      - "traefik.http.routers.gitea.entrypoints=https"
      - "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
      - "traefik.http.routers.gitea.service=gitea-service"
      - "traefik.http.services.gitea-service.loadbalancer.server.port=3000"
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

  gitea-cache:
    container_name: gitea-cache
    image: redis:6-alpine
    restart: unless-stopped
    networks:
      - public
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 15s
      timeout: 3s
      retries: 30
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

networks:
  public:
    name: public

Once your configuration is updated, start gitea:

docker-compose up -d

Creating network "public" with the default driver
Creating gitea-traefik ... done
Creating gitea-cache   ... done
Creating gitea         ... done

Once the containers has started, verify that they are all up:

docker-compose ps
    Name                   Command                  State                                       Ports
--------------------------------------------------------------------------------------------------------------------------------------
gitea           /usr/bin/entrypoint /bin/s ...   Up             0.0.0.0:222->22/tcp,:::222->22/tcp, 3000/tcp
gitea-cache     docker-entrypoint.sh redis ...   Up (healthy)   6379/tcp
gitea-traefik   /entrypoint.sh --api --pro ...   Up             0.0.0.0:443->443/tcp,:::443->443/tcp, 0.0.0.0:80->80/tcp,:::80->80/tcp

Installation and Configuration

Head over to the ROOT_URL of your gitea installation, in my case it looked like the following:

gitea-container

If you are not automatically redirected to register an account, select "Register" in the top right side:

gitea-register

If you would like to make use of email, configure your email settings here:

gitea-email

Then configure the admin account:

gitea-admin-account

Once you are logged in, you should see the following screen:

image

SSH Key

Now we would like to create a SSH key so that we can authorize our git client to pull and push to/from Gitea:

ssh-keygen -f ~/.ssh/gitea-demo -t rsa -C "Gitea-Demo" -q -N ""

Then head to your profile, select settings:

image

Select the SSH Tab and select "Add Key":

image

Head back to your terminal and copy your public ssh key from the key that we created earlier:

cat ~/.ssh/gitea-demo.pub
ssh-rsa AAAAB[x----redacted----x]/en5QDz3vI18n1u4lrKu1YsTR57YL Gitea-Demo

Then paste the public key into the form and add the key, you should then see the key present in gitea:

image

Create a Git Repository

Now head back to the "Dashboard", then select the "+" sign at the top and create a "New Repository":

image

From the repo form, I will be naming my repository "hello-world":

image

Then I selected "Initialise repository with Readme" and I selected to create the repository:

image

Now when we select the repo, we should see it in the Gitea UI:

image

To clone the repository via SSH, select the "SSH" button and click copy to clipboard:

image

Before we clone the repo on our terminal, let's setup the ssh-agent to be active for 1 hour:

eval $(ssh-agent -t 3600)

Then add the ssh key to the ssh-agent:

ssh-add ~/.ssh/gitea-demo

Identity added: ~/.ssh/gitea-demo (Gitea-Demo)

Optional: If you have a non default ssh key, like the above and you don't want to make use of ssh-agent you can setup a SSH Config, for example in ~/.ssh/config:

# Globals
Host *
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  #AddKeysToAgent yes
  #IdentityFile ~/.ssh/id_rsa
  ServerAliveInterval 60
  ServerAliveCountMax 30

# Gitea
Host git.rbkr.xyz
  IdentityFile ~/.ssh/gitea-demo
  User git
  Port 222

Now let's clone the repository:

git clone ssh://git@git.rbkr.xyz:222/ruanbekker/hello-world.git

Cloning into 'hello-world'...
Warning: Permanently added '[git.rbkr.xyz]:222,[95.x.x.x]:222' (ECDSA) to the list of known hosts.
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

Change into the directory which we cloned:

cd hello-world

Let's update the README.md file with any content, then after we saved the file, we can see that the file has been changed:

git status

On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

Add the file, commit and push to master:

git add README.md
git commit -m "Update readme for blogpost"
git push origin master

Writing objects: 100% (3/3), 305 bytes | 305.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://git.rbkr.xyz:222/ruanbekker/hello-world.git
   7804b67..85550dd  master -> master

View the changes

When we head back to the Gitea UI, we can see the README file has been updated, and we can see a git commit sha as well:

gitea-ui

In order to see what changed, we can click on the git commit sha:

gitea-commit

Swagger API

Gitea ships with Swagger by default and the endpoint is /api/swagger which in my case is accessible via:

And it looks like the following:

gitea-swagger

Thank You

I hope this was helpful, I was really impressed with Gitea. If you liked this content, please make sure to share or come say hi on my website or twitter: