How to switch from GitHub to your own Gitea environment

Introduction

For years, GitHub has been the undisputed top dog of code hosting. It is the default home for open-source projects and enterprise repositories alike. However, a growing number of developers are actively looking for alternatives to reclaim something crucial: control over their own code. Enter Gitea — a lightweight, blazing-fast, and self-hosted Git service that puts you back in the driver’s seat.

Why make the switch? The primary motivation boils down to data sovereignty and self-hosting. When you host your own Gitea instance, you own your infrastructure and dictate exactly who has access to your repositories. This desire for control has intensified alongside GitHub’s aggressive push to integrate AI into every facet of its platform. As highlighted in growing community critiques—perfectly summarized by the sentiment, “I’m tired of pretending GitHub is fine”—there is mounting frustration with the platform’s trajectory. Between UI bloat, aggressive AI feature rollouts, and lingering concerns over how user code is scraped and utilized for training models, many developers feel GitHub has lost sight of its core purpose. Sometimes, you just need a fast, reliable, and private environment to push your code, free from corporate AI experiments.

This guide goes beyond the “why” and provides a comprehensive, hands-on roadmap for the “how.” In the following sections, we will cover:

  • Technical Deployment: How to spin up a robust Gitea instance using Docker, including the setup of a Gitea Action Runner for your CI/CD pipelines.
  • Smart Migration Strategies: How to transition your repositories from GitHub to Gitea without disrupting your workflow. We will discuss the pros and cons of mirroring versus converting repositories to read-only.
  • The .github README Trick: A clever workflow hack that uses a secondary README.md to automatically notify GitHub visitors that a repository has moved to your self-hosted Gitea instance, without cluttering your actual project files.

Gitea Deployment

The most robust and flexible way to self-host Gitea is by using Docker. It keeps your host system clean, makes updates trivial, and allows for easy backups. For this deployment, we will use Docker Compose.

Here is the compose.yaml configuration to spin up the Gitea server:

services:
  server:
    image: docker.gitea.com/gitea:latest
    container_name: Git
    environment:
      - USER_UID=1026
      - USER_GID=100
      - TZ=Europe/Berlin
    restart: always
    networks:
      - git
    volumes:
      -  ./data:/data
    ports:
      - "3000:3000"
      - "222:22"

networks:
  git:
    external: true

Understanding the Parameters and Folder Structure

If you are running this on a NAS system or a custom home server, permission management is critical. The USER_UID and USER_GID variables ensure that the Gitea container writes files to your host system using the correct user permissions. For example, 1026 and 100 are common IDs for a standard user and the general users group on systems like Synology DSM. Setting these appropriately prevents frustrating permission denied errors when you try to back up or modify your repository data later.

Equally important is the persistent storage, which is handled by the volume mapping. The configuration line routing your host directory to the container’s data folder is the heart of your setup. Everything Gitea needs, from your repositories and the database to user avatars and configuration files, will be neatly contained within this single host directory. Finally, network access is established through port configurations. We map port 3000 to access the web interface. Additionally, we map port 222 to the container’s internal port 22 for Git SSH access, because the default port 22 is usually already occupied by the host system’s own SSH service.

Initial Setup and Database Choice

Once you bring the container up, navigate to the designated local IP address and port 3000 in your browser. Although I would recommend to set up a sub domain and reverse proxy to access Gitea. Anyways, once you open up the ip address or domain you will be greeted by the initial setup screen. A key decision here is the database. For enterprise environments, PostgreSQL or MySQL might be standard. However, for a single-hoster solution or a small team, SQLite3 is highly recommended. It requires zero additional configuration, no extra Docker containers, and offers more than enough performance for personal repositories.

Applying Configuration Changes via app.ini

While the web interface handles the basics, Gitea’s true power lies in its configuration file, known as app.ini. Because of how we structured our Docker volume, you can easily find this file on your host machine within the data directory under gitea/conf/app.ini. This file allows you to customize almost everything, from tweaking user interface settings and email notifications to disabling public registration completely. However, whenever you make manual changes to the app.ini file, you need to restart the Git container for them to take effect.

Optional: OIDC Configuration

For those who use a centralized identity provider for their self-hosted services, Gitea supports OpenID Connect out of the box. While you can configure this directly in the configuration file, the easiest way is to go to the Site Administration panel in the Gitea web backend. From there, navigate to Identity & Access, select Authentication Sources, and add your identity provider details. When setting this up, it is crucial to configure the callback URL correctly in your identity provider. The required format is 

https://git.domain.ltd/user/oauth2/ID/callback

In this structure, the ID portion must exactly match the value you entered in the Authentication Name field during the Gitea setup process. Getting this right ensures that your identity provider can successfully route authenticated users back to your Gitea instance, allowing you to manage logins securely using your existing Single Sign-On infrastructure.

Gitea Runner Deployment

While the Gitea server excels at managing your repositories, handling user access, and tracking issues, it is fundamentally a storage and management application. By itself, Gitea cannot compile your code, run your test suites, or deploy your applications. To bridge this gap and replicate the seamless automation provided by GitHub Actions, you need a dedicated execution engine. This is where the Gitea Runner, also known as the act_runner, comes into play. The runner operates as a separate worker process that constantly listens for instructions from your Gitea instance. Whenever an event triggers a workflow, such as pushing a new commit or opening a pull request, the runner picks up the job, spins up the necessary environments, and executes your scripts. Without it, your CI/CD pipelines simply would not exist.

To set this up, you do not need a completely separate server. You can seamlessly extend your existing Docker infrastructure by adding the runner as a secondary service to your compose.yaml file. This approach ensures that your automation environment stays coupled with your Git instance while remaining securely isolated in its own container.

runner:
    image: gitea/act_runner:latest
    container_name: Gitea-Runner
    restart: always
    environment:
      - CONFIG_FILE=/config.yaml
      - GITEA_INSTANCE_URL=https://git.domain.ltd
      - GITEA_RUNNER_REGISTRATION_TOKEN=ADD_YOUR_TOKEN_HERE
      - GITEA_RUNNER_NAME=Gitea-Runner
      - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:18-alpine,linux:docker://node:18-alpine
    volumes:
      - ./data:/data
      - ./config.yaml:/config.yaml
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - git

Understanding Volume Mapping and the Docker Socket

A critical part of this configuration is – again – the volume mapping. The line mapping config.yaml allows you to inject fine-grained runner settings directly into the container without rebuilding it. Even more vital is the mapping of /var/run/docker.sock. By giving the runner access to the host’s Docker socket, you enable it to spawn ephemeral containers to execute your build jobs. This is essentially a Docker-out-of-Docker communication model, which is significantly more efficient on home servers and NAS hardware than running a full nested Docker-in-Docker setup. It allows the runner to utilize the host’s engine to pull images and run tests as if it were a native process.

The Runner Configuration and Labels

To make the runner behave correctly, you need a basic config.yaml file located in your host directory. This file dictates how the runner interacts with the Docker daemon and the network.

container:
  privileged: true
  docker_host: "unix:///var/run/docker.sock"
  network: "git" 
  valid_volumes:
    - /var/run/docker.sock
  rm_adhoc: true

In this configuration, we set privileged: true to ensure the runner has the necessary permissions to manage containers effectively. Setting the network to git is essential because it allows the runner to communicate directly with the Gitea server container via the internal Docker network.Both are part of the git network in our setup here. Furthermore, the GITEA_RUNNER_LABELS environment variable in the compose file plays a strategic role. By mapping ubuntu-latest to a specific, lightweight image like node:18-alpine, you can use existing GitHub Action workflows that reference runs-on: ubuntu-latest without having to modify every single repository. The runner simply sees the request for Ubuntu and fulfills it using your defined, resource-efficient Alpine-based container.

Enabling Actions in the Server Configuration

Before finalizing the integration, there is one crucial server-side step that is easily overlooked, often resulting in a frustrating scenario known as the “silent runner” issue. By default, Gitea usually has its Actions feature disabled globally. If you skip this step, you could set up the runner perfectly, see it register with a healthy green status in the backend, push your workflow files, and then wonder why absolutely nothing triggers. To prevent this, you need to revisit your main Gitea server’s configuration file. Open your app.ini file located on your host machine within your mapped data directory. Scroll to the bottom of the file and add a dedicated section for actions to explicitly turn the feature on:

[actions]
ENABLED = true

Save the file and restart your primary Gitea server container so the changes can take effect. Once the server reboots, the Actions tab will become visible across your repositories and organization settings, ensuring that your newly configured runner actually receives the jobs it is waiting for.

Finalizing the Integration

Before you start the runner service, you must retrieve the registration token from your Gitea web backend. You can find this under Site Administration, then Actions, and finally Runners.

Once you have pasted this token into your environment variables and started the container, the runner will automatically register itself. You will see it appear in the Gitea dashboard with a green status, indicating it is ready to pick up jobs. From this point forward, any time you push code with a .gitea/workflows or .github/workflows file, your self-hosted runner will spring into action, providing the same automation experience you expect from GitHub but entirely on your own hardware.

Migrating GitHub Repositories to Gitea via Web Frontend

Before initiating the actual migration process, you need to generate personal access tokens on both platforms. On GitHub, navigate to your developer settings and create a classic token with scopes enabled for repository access and workflows. Simultaneously, on your new Gitea instance, generate a similar token under your user settings within the applications tab. These tokens act as secure digital keys, allowing the two systems to authenticate and communicate securely during the transfer process.

With your tokens ready, the standard migration workflow is remarkably straightforward. In the Gitea web interface, click the plus icon in the top right corner to create a new repository, but select the option to migrate from another platform instead of starting fresh. Choose GitHub as your source, input your existing GitHub repository URL, and provide the GitHub access token you just generated. Gitea will seamlessly pull in your code, branches, commit history, and even your issues and pull requests, depending on the options you select.

At this stage, you face a crucial architectural decision: will Gitea serve as your primary development hub, or is it merely a backup to your existing setup? The migration tool offers a checkbox to set up the new repository as a mirror. If Gitea is simply a secondary backup, setting up a pull mirror from GitHub to Gitea is the right choice. However, if your goal is true data sovereignty and you intend to make Gitea your main system for your personal and professional code repositories, you should approach mirroring differently.

When transitioning fully to Gitea, a common instinct is to archive your old GitHub repositories or make them strictly read-only. However, a more elegant best practice exists. Instead of locking down the old repository entirely which prohibits mirroring functionalities at all, you can configure a push mirror from Gitea back to GitHub. This keeps your public GitHub presence active and serves as an off-site backup.

To smoothly inform external contributors or visitors that active development has moved, you can utilize a clever trick with the .github directory. Create a second README.md file specifically inside the .github folder of your Gitea repository. GitHub’s web interface is designed to prioritize a readme file located in the .github directory over the one situated in the root folder. This allows you to display a prominent notice on your GitHub repository page explaining the migration and linking directly to your new self-hosted Gitea instance. Meanwhile, your actual root README.md remains untouched, displaying your normal project documentation cleanly within your local development and Gitea environment without any manual editing required on GitHub. This trick allows for managing all necessary things within your new Gitea environment.

A notice like this could look like this e.g.:

# ⚠️ This repository has moved!

Active development and maintenance of this project are **no longer taking place on GitHub**. I have migrated my projects to my own Gitea instance. 

This GitHub repository will be archived (read-only) shortly and will only serve as a reference / read only mirror. Unfortunately, new Issues, Pull Requests, or discussions can no longer be processed here.

## 🚀 New Project Location

You can find the latest source code, new releases, and the issue tracker for this specific project right here:

👉 **https://git.domain.ltd/user/repo**

## 👤 My New Profile

If you want to continue following my work or are interested in my other projects, you can find my entire portfolio on my new Gitea profile:

🔗 **[https://git.domain.ltd/user](https://git.domain.ltd/user)**

Thank you for your support on GitHub – see you on Gitea!

Finally, to prevent fragmented communication and ensure a clean cut-over, you should navigate to the repository settings on GitHub and restrict the permissions. Specifically, you should disable the ability for users to create new issues, open discussions, or submit pull requests. This guarantees that all community interaction and actual development work are funneled exclusively to your new, self-hosted environment.

Updating Local Repositories to Point to Gitea

Migrating your repositories on the server is only the first half of the process. If you have been actively developing, you likely have local clones of these projects sitting on your hard drive, all of which are still configured to push and pull from GitHub. Rather than deleting these local folders and re-cloning everything from your new Gitea instance, you can simply redirect their remote origin URLs to point to your new self-hosted server. This approach preserves your local uncommitted changes, stashes, and overall workspace state.

The most efficient way to achieve this redirection is through the terminal. By navigating into your local repository’s root directory, you can execute a single Git command to update the upstream pointer. Running git remote set-url origin followed by your new Gitea repository URL instantly reroutes all future push and pull operations.

or example, utilizing the server address from our previous setup, you would type

git remote set-url origin https://git.domain.ltd/user/repo.git

and hit enter.

If you prefer using SSH instead of HTTPS, remember that we mapped Gitea’s SSH traffic to port 222 in our Docker compose file. Your command will look like this:

git remote set-url origin ssh://git@git.domain.ltd:222/user/repo.git

Note: Don’t forget to upload your public SSH key to your new Gitea profile settings!)

You can easily verify that the change was successful by running git remote -v, which will print out the updated fetch and push addresses, confirming that your local codebase is now communicating exclusively with your Gitea instance.

f you prefer a more hands-on approach or simply want to understand what the terminal command is doing under the hood, you can manually edit the Git configuration file. Inside the root folder of your local repository, there is a hidden directory named .git which contains a plain text file called config. Opening this file in any text editor will reveal a section specifically labeled [remote “origin”]. Directly beneath this heading, you will find a url = line containing your old GitHub web address. By replacing that old address with your new Gitea repository URL and saving the file, you accomplish the exact same redirection as the terminal command.

Finally, if you rely on graphical user interfaces to manage your version control, you do not need to abandon your preferred workflow. Tools like GitHub Desktop, and particularly enhanced community forks like GitHub Desktop Plus, continue to function seamlessly with Gitea. Once you have updated the remote URL using either the terminal or the text editor method, your GUI client will automatically detect the new origin. When you attempt your first sync, the application will simply prompt you for your Gitea credentials where you can just put in your personal access token you created earlier, allowing you to resume your regular development routine without any disruption.

Conclusion

Making the switch from a ubiquitous platform like GitHub to a self-hosted Gitea instance might initially seem like a daunting technical hurdle, but as we have explored, the process is surprisingly manageable. By leveraging Docker for a clean deployment, configuring a dedicated runner for uninterrupted CI/CD workflows, and utilizing smart migration strategies like the .github repository trick, you can transition your entire development environment without sacrificing functionality or your public community presence.

Ultimately, this migration is about much more than just changing the URL where your code lives. It is a fundamental step toward true data sovereignty and digital independence. In an era where centralized platforms increasingly monetize user activity and train AI models on vast swaths of repository data, hosting your own infrastructure guarantees that you remain the sole owner and controller of your intellectual property. You gain the peace of mind that comes with knowing exactly where your data resides and who has access to it. Taking back control over your codebase is a profoundly satisfying milestone, ensuring that your development tools serve your needs first, rather than the other way around.

Leave a Reply

Your email address will not be published. Required fields are marked *