Push to Multiple Git Servers

If you develop software, you probably use Git for version control and GitHub for hosting your repositories. GitHub is a nice platform, but recently it has been a little unreliable. I also like to fully own my code and having some redundancy on my services and files.

With this in mind I like to have multiple remotes for my Git repositories, so that I can push to multiple instances at the same time. Gitea is a great open source self-hosted Git server that I use for this purpose. Its lightweight nature and ease of use make it an excellent choice for hosting my own repositories.

The goal is to automatically push code to both GitHub and a private self-hosted instance of Gitea and keep them in sync. If one of them goes down or experiences issues, I can still access my code from the other.

#Setting Up a Gitea Self-Hosted Instance

If you already own a server, you can install Gitea on it. If not, you can use a cloud provider to host your Gitea instance. A basic server, for example with 2 vCPU (~2.0 GHz per vCPU) and 2GB of RAM, should be sufficient for medium-sized repositories and usage.
First, you need to set up your Gitea instance. You can choose the method and setup that best suits your needs, but for simplicity, I like to install it using Docker Compose and make it use a Postgres database like explained in the official documentation.

Once you have Gitea up and running, create a new repository for your project on the Gitea web interface. Copy the SSH URL for the repository, as you will need it later to configure your Git remotes. Throughout this article we assume that your Gitea server is running on your_gitea_server.com at port 2222 and the repository is located at your_user/project.git.

#Setting Up SSH Keys for Multiple Git Servers

If you use SSH to authenticate with Git servers (recommended), you need to generate and register an SSH key. You can use the same key for both servers, usually I have separate keys for personal vs work. But if you want to keep them separate per server, you can generate different keys for each server and configure your SSH client to use the correct key for each server.

1. Generate SSH keys

Generate a dedicated key for GitHub and one for Gitea:

ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_github
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_gitea

macOS tip: protect your keys with a passphrase and store it in the Keychain.

When ssh-keygen asks for a passphrase, set one. It encrypts the private key file, so even if someone steals the file, they cannot use it without the passphrase. If you already generated a key without a passphrase, you can add one later with ssh-keygen -p -f ~/.ssh/id_ed25519_github.

To avoid typing the passphrase every time, macOS can store it in the Apple Keychain and let the SSH agent retrieve it automatically. Add the keys to the agent with the Keychain integration (on macOS 12 Monterey and later; on older versions use -K instead of --apple-use-keychain):

ssh-add --apple-use-keychain ~/.ssh/id_ed25519_github
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_gitea

Then, when configuring your SSH client (step 3 below), also add the following block to your ~/.ssh/config:

Host *
    AddKeysToAgent yes
    UseKeychain yes
  • AddKeysToAgent yes adds a key to the SSH agent the first time you use it, so you don’t need to run ssh-add again. For every subsequent connection using that key, SSH finds it already loaded in the agent and uses it directly, so you’re not prompted for the passphrase again.
  • UseKeychain yes (macOS only) stores and reads the passphrases in the Apple Keychain, so you are not prompted for them again.

You can list the keys currently loaded in the agent with ssh-add -l.

Note: UseKeychain and --apple-use-keychain are Apple-specific options. On Linux, use your desktop environment’s keyring or a plain ssh-agent. On Windows, use the OpenSSH Authentication Agent service.

2. Add the public keys to each server

cat ~/.ssh/id_ed25519_github.pub
  • Copy the Gitea public key and add it to your Gitea SSH keys settings (https://your_gitea_server.com/user/settings/keys) at the website of your Gitea instance:
cat ~/.ssh/id_ed25519_gitea.pub

3. Configure the SSH client

Create or edit your ~/.ssh/config file to tell SSH which key to use for each host:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_github
    IdentitiesOnly yes

Host your_gitea_server.com
    HostName your_gitea_server.com
    Port 2222
    User git
    IdentityFile ~/.ssh/id_ed25519_gitea
    IdentitiesOnly yes

4. Test the connections

Verify that SSH authentication works for both servers:

ssh -T [email protected]
ssh -T git@your_gitea_server.com

You should see a welcome message from each server confirming the connection is working.

Note: Keep your private keys (the corresponding files without .pub extension) secure and do not share them. If you need to use the same key for both servers, you can simply add the same public key to both GitHub and Gitea, and configure your SSH client to use that key for both hosts.

#Choosing the Right Approach

You can configure Git to push to multiple remotes in two main ways: having a single remote with multiple push URLs or by creating separate remotes for each server. Here is a quick comparison of the two approaches to help you decide which one best fits your workflow.

FeatureSingle Remote and Multiple Push URLsTwo Separate Remotes
Simpler configuration
Push to all remotes with one command❌ (requires alias)
Suitable for simple backup/redundancy
Switch primary remote easily
Independent fetch per remote
Fine-grained control over remotes
Suitable for complex multi-remote workflows

If you just want redundancy and a simple workflow, go with the Single Remote and Multiple Push URLs approach. If you need more control over fetching and pushing independently, use the Two Separate Remotes approach.

#Single Remote and Multiple Push URLs

Configure the primary remote (GitHub). It sets the remote URL, which Git uses for fetching and also for pushing, as long as no push URL is configured:

# Fetch/pull from GitHub
git remote add origin [email protected]:your_user/project.git

# Push to GitHub (must be explicit: once any push URL exists, the fetch URL is no longer used for pushing)
git remote set-url --add --push origin [email protected]:your_user/project.git

Configure the secondary push remote (Gitea).

# Push to Gitea as well
git remote set-url --add --push origin ssh://git@your_gitea_server.com:2222/your_user/project.git

If you want, later, to remove the push URL for Gitea, you can do the following command:

git remote set-url --delete --push origin ssh://git@your_gitea_server.com:2222/your_user/project.git

Check the remotes configuration:

git remote -v

Example output:

origin  [email protected]:your_user/project.git (fetch)
origin  [email protected]:your_user/project.git (push)
origin  ssh://git@your_gitea_server.com:2222/your_user/project.git (push)

The meaning of this output is that the fetch and pull URL is set to GitHub while the push URLs are set to both GitHub and Gitea. When you run git fetch or git pull, it will only fetch from GitHub. When you run git push, it will push to both remotes.
With this setup GitHub is the primary remote for fetching, your source of truth, and Gitea is a secondary remote for pushing, providing redundancy and backup for your code. If GitHub later becomes unavailable or you want to abandon it, you can change the fetch URL (of origin) to Gitea and continue working without interruption:

# 1. Point fetch/pull at Gitea
git remote set-url origin ssh://git@your_gitea_server.com:2222/your_user/project.git

# 2. Stop pushing to GitHub
git remote set-url --delete --push origin [email protected]:your_user/project.git

If you change your mind, again, and want to switch back to GitHub as the primary remote for fetching, you can change the fetch URL:

git remote set-url origin [email protected]:your_user/project.git
git remote set-url --add --push origin [email protected]:your_user/project.git

Pushes to multiple push URLs can fail independently (non-fast-forward). Check exit status and fix by pulling/merging on the remote that rejects. You can always remove the failing push URL and add it back later when the issue is resolved.

#Two Separate Remotes

If you prefer to keep the remotes configuration separate, you can add a new remote for Gitea instead of adding a push URL to the existing remote:

git remote add gitea ssh://git@your_gitea_server.com:2222/your_user/project.git

If you follow this approach of two separate remotes, you will need to push to both remotes separately to keep them in sync:

git push origin
git push gitea

It’s possible to set a git alias to push to both remotes with a single command. The ; between the commands makes both pushes run even if the first one fails. Note that the exit status of the alias comes from the last command, so if the push to origin fails but the push to gitea succeeds, the alias still reports success. Be aware of this if you use the exit status in scripts or automation:

git config --global alias.pushall '!git push origin; git push gitea'

Then you can simply run git pushall to push to both remotes at the same time.

On this separate remotes configuration, it fetches from the remote your current branch is tracking. If you push with -u (or set push.autoSetupRemote=true), Git sets the tracking remote to the one you pushed to. So if you push to origin (GitHub) first, your branch will track origin/branch-name. If none is set as the tracking remote, Git will fetch from the default remote (origin) which is GitHub in this case.

If you want to fetch and pull from Gitea, you can do the following commands:

# Push current branch to the remote named gitea and set upstream (this will set the current branch to track the remote branch with the same name on gitea, creates the remote branch if it doesn't exist)
git push -u gitea HEAD

# If you want the remote branch to have a different name
git push -u gitea HEAD:remote-branch
# or
git push -u gitea local-branch:remote-branch

# Set upstream (without push) for the current branch to gitea/remote-branch, remote-branch should already exist
git branch --set-upstream-to=gitea/remote-branch

Upstream is the branch your current branch “tracks”. Git uses it as the default upstream for pull, fetch/merge, push, and to show ahead/behind in status.

To check which remote your current branch is tracking, you can use the following commands:

git branch -vv
git config --get branch.$(git symbolic-ref --short HEAD).remote

This approach gives you more control over which remote you fetch from and push to, at the cost of running more commands to keep both remotes in sync. If you’d rather push to both with a single command, the single remote with multiple push URLs is more convenient.

Remember that git fetch only updates your local references and does not merge any changes into your current branch, while git pull is a combination of git fetch followed by git merge FETCH_HEAD, which updates your local references and merges the changes into your current branch. When you have multiple remotes, you can choose to fetch from one remote and pull from another, depending on your workflow and preferences:

git fetch origin
git fetch gitea
git pull origin main
git pull gitea main

#Common Pitfalls and Best Practices

Note that git pull --all does not pull from all remotes, it only pulls from the current branch’s tracking remote, which is the default remote for the current branch. git pull --all runs git fetch --all (fetches each remote sequentially) and then merges or rebases only the current branch from its configured upstream, it does NOT merge changes from every remote or update every local branch:

git pull --all

Is equivalent to the following commands:

git fetch --all
git merge FETCH_HEAD # or rebase, depending on pull.rebase configuration

#Simple Workflow With Single Remote and Multiple Push URLs

If you want to keep it simple and push to both remotes with just a single command, the configuration with a single remote and multiple push URLs is the way to go.

Push current branch to both remotes (GitHub and Gitea):

git add .
git commit -m "feat: your commit message"
git push

Push all local branches to origin and to all origin’s push URLs (GitHub and Gitea):

git push --all

Push all local tags to origin and to all origin’s push URLs (GitHub and Gitea):

git push --tags

#Conclusion

I run a private Gitea instance and push to both GitHub and Gitea at the same time. Configuring multiple push URLs for my Git remote keeps my code backed up on both GitHub and my self-hosted Gitea instance. If you don’t want to maintain your own server, a similar approach works with other Git service providers, for example GitLab or Codeberg. This setup provides a good balance between convenience and redundancy, ensuring that your code is safely stored on multiple platforms.

Date: