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: ```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 ?