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 undercertificatesResolvers.letsencrypt.acme.email
traefik.rbkr.xyz
with 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:
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
andSSH_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:
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:
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:
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:
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
- Twitter: @ruanbekker