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.comwith your email undercertificatesResolvers.letsencrypt.acme.email -
traefik.rbkr.xyzwith your fqdn for traefik undertraefik.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:
```bash
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:
- https://docs.gitea.io/en-us/install-with-docker/#databases
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`:
```yaml
---
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:
```yaml
---
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:
```bash
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:
```bash
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:

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

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

Then configure the admin account:

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

## 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:
```bash
ssh-keygen -f ~/.ssh/gitea-demo -t rsa -C "Gitea-Demo" -q -N ""
```
Then head to your profile, select settings:

Select the SSH Tab and select "Add Key":

Head back to your terminal and copy your public ssh key from the key that we created earlier:
```bash
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:

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

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

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

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

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

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:

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

## Swagger API
Gitea ships with Swagger by default and the endpoint is `/api/swagger` which in my case is accessible via:
- https://git.rbkr.xyz/api/swagger
And it looks like the following:

## 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:
* **Website**: [ruan.dev](https://ruan.dev)
* **Twitter**: [@ruanbekker](https://twitter.com/ruanbekker)
Top comments (1)
Hey, thanks for the great article.
How do I remove the need to add :222 to the clone ssh url ?
Is there a way to use 22 instead ?
Do I simply need to make sshd listen on another port to let gitea use it ?
Or can I somehow make them both use it ?