Let's Connect

A rack of servers in a data centre, representing the choice between self-hosting GitLab CE on your own infrastructure and using managed GitHub

The self-hosted GitLab vs GitHub decision is not about which tool is better, because both are excellent. It is about whether you want to own your Git infrastructure or rent it. I have run GitLab CE on a 4 vCPU EC2 instance for a team that needed code to stay on our own servers, and I have shipped plenty of repos on GitHub where I never once thought about patching, backups, or disk pressure. The tradeoff is ownership versus maintenance: self-hosted GitLab gives you total control, integrated CI, and no per-seat bill, but you are now the sysadmin who patches it, backs it up, and gets paged when it falls over. GitHub hands you the best ecosystem and zero ops in exchange for your code living on their infrastructure and a cost that grows with seats and CI minutes.

What does self-hosting GitLab actually cost you in effort?

GitLab CE (Community Edition) is free, open source, and genuinely complete: repos, merge requests, issues, a container registry, a package registry, and a full CI/CD engine, all in one install. The Omnibus package bundles everything (PostgreSQL, Redis, Nginx, Sidekiq, Puma) into a single deb so the install really is a few commands. The cost shows up after the install.

It is resource-hungry. The official minimum is 4 GB of RAM, and that is a floor, not a comfortable number. On 2 GB it will swap and crawl. Below is the install on Ubuntu 22.04 / 24.04 LTS. Note that you set the root password via an environment variable on first run, otherwise it is written to a file you then have to go read.

install-gitlab-ce-ubuntu.sh
# Dependencies
sudo apt-get update
sudo apt-get install -y curl openssh-server ca-certificates tzdata perl

# Add the official GitLab CE repository
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash

# Install. EXTERNAL_URL drives the bundled Nginx vhost; set the
# initial root password so you are not hunting for it afterwards.
sudo EXTERNAL_URL="https://gitlab.example.com" \
     GITLAB_ROOT_PASSWORD="change-me-to-something-long" \
     apt-get install -y gitlab-ce

# Apply config changes after editing /etc/gitlab/gitlab.rb
sudo gitlab-ctl reconfigure

Then the ongoing work begins. You patch it: GitLab ships a security release roughly every month, and CVEs in the web layer are real, so "I will update it later" is how you end up running a vulnerable Git host on the public internet. You back it up: a single command produces a tarball, but a backup you have never restored is a hope, not a backup. And you scale it: the day CI gets busy, Sidekiq queues back up and merge requests feel sluggish, and that is your problem to diagnose.

backup-gitlab.sh
# Backs up repos, DB, uploads, and CI artifacts to
# /var/opt/gitlab/backups by default.
sudo gitlab-backup create

# CRITICAL: the application backup does NOT include your secrets
# and config. Without these two files the restore is useless.
sudo cp /etc/gitlab/gitlab-secrets.json /secure/offsite/
sudo cp /etc/gitlab/gitlab.rb          /secure/offsite/

That gitlab-secrets.json gotcha has bitten people badly. The backup tarball contains your data encrypted with keys that live in that secrets file. Restore the tarball onto a fresh box without the matching secrets and your CI variables, 2FA settings, and integration tokens are unrecoverable. If you self-host, automate copying both files off-box, the same way you would for any stateful server. My notes on server hardening basics for web developers cover the rest of keeping a box like this safe to expose.

Network and server cabling in a rack, illustrating the infrastructure you take ownership of when you self-host GitLab CE
Self-hosting means this is yours: the patching, the backups, the disk that fills up at 2 a.m. Control has an operational price.

Why pick managed GitHub instead?

GitHub's pitch is the inverse: you do zero operations and you plug into the largest ecosystem in the industry. There is no server to patch, no backup to test, no disk to monitor; uptime and scaling are GitHub's job. The integration surface is enormous, almost every SaaS tool, security scanner, and deploy target has a first-class GitHub integration, and the Actions marketplace means most CI steps you need already exist as a maintained action you can drop in.

The tradeoff is equally clear. Your code and your CI run on GitHub's infrastructure, which is a non-starter for some compliance regimes and a deliberate choice you should make consciously rather than by default. And the cost scales with usage. You pay per seat above the free tier, and Actions bills by the minute past the included allowance, with the meter running faster on larger runners and at a premium for macOS and Windows. A chatty matrix build on big runners can produce a CI bill that surprises you, the same way an unwatched cloud account does, which I wrote about in reducing your AWS bill.

Self-hosting does not remove cost, it converts a predictable monthly invoice into your own time, plus the risk that the time arrives all at once during an incident.Md Raihan Hasan

How do the CI runners actually differ?

Both have a YAML pipeline at the repo root and both farm jobs out to runners, but the runner model is where day-to-day life diverges.

GitLab Runner

On self-hosted GitLab you almost always run your own GitLab Runner. You install the agent, register it against your instance with an authentication token, and pick an executor, typically docker, so each job runs in a clean container. The win is that runners are yours: the real cost is the EC2 box or on-prem hardware they sit on, you can give them access to your private network, and there are no per-minute charges. The cost is that those runners are one more thing you install, register, update, and keep healthy.

register-gitlab-runner.sh
# After installing the gitlab-runner package on a worker box.
# Modern GitLab (16+) uses a glrt- authentication token created
# in the UI; --registration-token is the old, removed flow.
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com" \
  --token "glrt-XXXXXXXXXXXXXXXXXXXX" \
  --executor "docker" \
  --docker-image "alpine:3.20" \
  --description "docker-runner-1"

A minimal .gitlab-ci.yml defines a stage and a job; the docker executor you registered runs each job in the image you name:

.gitlab-ci.yml
stages:
  - test

unit-tests:
  stage: test
  image: node:20-alpine
  script:
    - npm ci
    - npm test

GitHub Actions

On GitHub the default is GitHub-hosted runners: ephemeral VMs GitHub spins up per job, fully managed, billed per minute. You write nothing to provision them, you just say runs-on: ubuntu-latest. When you need private-network access or custom hardware you can attach self-hosted runners, but the common path is to use theirs and pay for the minutes. The deeper pipeline mechanics, caching, matrix builds, secrets, are the same shape as GitLab; I walk through a full deploy pipeline in CI/CD with GitHub Actions.

.github/workflows/test.yml
name: test
on: [push]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

How hard is it to migrate between them?

Moving the code is easy; moving the pipeline is the work. Git history is portable by definition, a bare mirror clone and a push to the new remote moves every branch and tag intact, and both platforms have importers that also carry issues and merge/pull requests over the API.

mirror-repo-to-new-remote.sh
# Full history, all branches and tags, no working tree.
git clone --mirror git@github.com:acme/app.git
cd app.git

# Push everything to the new home (GitLab here, or reverse it).
git remote set-url origin git@gitlab.example.com:acme/app.git
git push --mirror

What does not migrate is the CI definition. .gitlab-ci.yml and a GitHub Actions workflow are different schemas with different concepts (stages and jobs vs jobs and runs-on, GitLab CI/CD variables vs Actions secrets, GitLab include vs reusable workflows). There is no clean automatic converter you should trust for anything non-trivial; budget time to rewrite the pipeline by hand and re-test it. Plan the move around the pipeline, not the repos.

So which should you choose?

After running both, my decision list is short:

  • Choose self-hosted GitLab CE if: your code must stay on infrastructure you control (compliance, data-residency, or air-gapped), you want repos, registry, and CI in one product with no per-seat cost, AND you have someone willing to own patching, backups, and scaling as a real responsibility.
  • Choose GitHub if: you want zero operational burden, the broadest integration and Actions ecosystem, and managed reliability, and you are comfortable with your code on their infrastructure and a bill that scales with seats and CI minutes.
  • Lean GitHub for solo developers and small teams: the free tier is generous and the time you would spend babysitting a GitLab box is worth more than the licence you saved.
  • Lean self-hosted GitLab as you grow past a dozen seats with strict data-control needs: at that scale per-seat pricing and the control argument both start to favour owning it, provided you have the ops capacity.
  • Do not self-host purely to save money on a tiny team: the server, your hours, and the on-call risk usually cost more than the subscription you avoided.

There is no universally correct answer here, only the right answer for your constraints. If your binding constraint is data control or you genuinely want everything in one self-owned platform, GitLab CE is excellent and I have run it happily for years. If your binding constraint is shipping software without becoming an infrastructure team, GitHub is the pragmatic default and there is no shame in renting. Pick the one whose downside, a monthly invoice or a maintenance burden, you would rather live with, because that is the part you will actually feel every week.