Why I Self-Host GitLab and Still Mirror to GitHub

AI agentsdraft playbooksGitLabself-hosted hubSemaphoreruns from gitNetworkswitch, firewall

All my automation lives in git, and for a while that meant GitHub and nothing else. It worked fine. But the enterprise world I work toward runs a lot of self-hosted GitLab, and the best way to learn a platform is to be the one responsible for it. So GitLab CE went into the lab as a Docker container, and it’s become the primary home for my repos.

The container

GitLab CE in compose form, with the three volumes that actually hold your instance and remapped ports so it doesn’t fight the host:

services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    hostname: gitlab.lab
    ports:
      - "8929:8929"    # http
      - "2224:22"      # git ssh
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://gitlab.lab:8929'
        gitlab_rails['gitlab_shell_ssh_port'] = 2224
    volumes:
      - ./config:/etc/gitlab
      - ./logs:/var/log/gitlab
      - ./data:/var/opt/gitlab
    shm_size: '256m'

Two details there that cost people evenings: external_url has to include the remapped port or every clone URL GitLab generates is wrong, and the SSH port remap needs both the ports entry and the gitlab_shell_ssh_port setting so the UI shows clone commands that actually work.

Being the somebody

Running your own git server changes your relationship with it. On GitHub, backups, upgrades, and runners are somebody else’s problem. On your own GitLab, you are the somebody. Backups became a scheduled job immediately, because a git server that isn’t backed up is a countdown timer:

docker exec gitlab gitlab-backup create
# secrets are NOT in the backup archive; copy these two separately:
docker cp gitlab:/etc/gitlab/gitlab-secrets.json ./
docker cp gitlab:/etc/gitlab/gitlab.rb ./

That second part is the one everyone learns the hard way. The backup archive deliberately excludes gitlab-secrets.json, and without it a restored instance can’t decrypt its own CI variables and tokens. A backup you haven’t restored is a rumor, so I tested the restore path into a scratch container before trusting any of it.

Mirroring to GitHub

I’m not abandoning GitHub. GitLab is primary, GitHub is a push mirror, which gets me full control and enterprise practice at home plus a public presence recruiters can find. Mirroring also means the lab burning down doesn’t take my code with it. One copy is no copies.

Setup is Settings, Repository, Mirroring repositories on each project, with a GitHub token that has repo scope only. One gotcha from my own cutover: if your GitHub account has email privacy turned on, mirror pushes fail with GH007 when any commit carries your private email. Set your git identity to the noreply address GitHub assigns you before the first commit of a new repo, or you’ll be rewriting history later:

git config user.email "1234567+username@users.noreply.github.com"

The rest of the pipeline plugs in behind it. Semaphore pulls playbooks from GitLab, config backups commit to a private repo on it, and the AI coding agents I’ve written about work against these same repos. The git server stopped being a place I push code and became the hub everything else hangs off. That’s worth self-hosting once, just to understand.