Gitlab Github and Gitea: A Comparison

Published: Aug 23, 2023 by Isaac Johnson

I was asked recently by Mehdi on Mastodon about Gitlab in comparison to Github and Gitea.

I realized I haven’t really covered Gitlab since my 3-part series last year (1, 2, and 3)

My summary then:

Gitlab offers quite a lot of features. It’s a well-rounded suite that goes toe-to-toe with offerings from Microsoft (Github and Azure DevOps) and Atlassian. I found the Issue Management was far more impressive than that found in Github, but I still felt JIRA and Azure DevOps are the best for Work Items/Agile. I found the runners worked as well as those found elsewhere, but generally I prefer the more selective nature of Azure DevOps on agent pools.

Let’s revisit Gitlab and compare with Github and Gitea


First let’s cover organization.

In the case of Gitea and Gitlab, Repos are organized under projects. In Github, we have “Organization” repos which can be actual orgs or a ‘project’, and user repos.



When looking to create a repo, we start by creating a Project



Github has personal repos under your own userid and organizational repos.


Github does have Projects but they are more about Issue Tracking than repo organization


In the case of Github, the concept that more aligns to the Gitlab Project is the “Organization” that allows me to group Issues and Repos with which to collaborate with others.



Gitea follows the Github model of user repos and Organizations


Like Github, it uses “Projects” just for Issue Management



All of these products offer some form of documentation as code by way of a built-in wiki system.


Gitlab has a nice simple Wiki editor in every project


You can see page history and view former versions, but not compare them


You can link pages, but otherwise the organization is flat


That said, pages are indexed by search and can be found using the global search



Wiki is under “Wiki” in any Github repo


We can easily whip up a page in markdown


The fact I linked to a non-existent page actually prompts an editor to create that page when clicked (like Confluence does)


If I try and link to “folder/page”, it thinks that “page” is a version, so this too shows that Github, like Gitlab, is flat in structure



Gitea also offers a wiki section under repos


I can create a Markdown based page with links and tables as before


However, clicking that dead link just takes me to an empty page (instead of a create page as Github does)


I can create “folder/page” but it is just a name.


In fact we can see it just encoded the slash in the URL


Public project wiki

In all three cases, public projects properly showed the Wiki giving developers an easy way to expose documentation.

I tested this using Incognito windows to ensure I wasn’t logged in


Mermaid Diagrams

All three systems supported MermaidJS in Markdown.

I used the following example:

` ` `mermaid
graph TB
    it(integration test) --> st(sanity test)
    it --> nst(non-sanity test)
    st --> pct(pre-commit test)
    st --> et(extended tests)
` ` `






Gitlab had a pretty nice built-in Diagram creator. Granted it’s one way - that is, you create and it saves an image and you’re done (no editing it later). Still, pretty handy for quick flow charts


In issues, we can always create a new issue with Create Issue


If I already have code in a branch, I can create an MR right from the issue which is pretty handy


I can also use “Move Issue” to move it between projects


Like Gitea, I can set a time tracking estimate


Some fields, like weight, are behind a “pro” version


We can also tie to JIRA to import issues


Above you can see the bi-directional nature where Gitlab put in links to to the JIRA story and we pulled in details from JIRA into Gitlab

We covered that a year ago, but to be clear, the JIRA setup is under Integrations and just needs an API key or password




In this public “dockerWithTests2” repo, I had created a pipeline that would build a container and push it to the Gitlab registry

$ cat .gitlab-ci.yaml
image: docker:20.10.16


  - docker:20.10.16-dind

  - build

# Build MR
  stage: build
  image: node:latest
    - echo "@nodewithtests:registry=">.npmrc
    - npm config set -- '//' "${CI_JOB_TOKEN}"
    - npm config set always-auth true
    - npm publish --registry

  stage: build
    - export
    - docker build --target test -t .
    - if: $CI_COMMIT_BRANCH != 'main'

# Build prod
  stage: build
    - set +x
    - set -x
    - docker build -t .
    - docker push
    - if: $CI_COMMIT_BRANCH == 'main'

While docker build all failed to push to the NPM registry

npm notice shasum:        997e2fd19a788755431ae6066a4f3faa2840a2e2
npm notice integrity:     sha512-muVwMZ8BOupGn[...]hIEcvuAUyJLKw==
npm notice total files:   23                                      
npm notice 
npm notice Publishing to
npm ERR! code E403
npm ERR! 403 403 Forbidden - PUT
npm ERR! 403 In most cases, you or one of your dependencies are requesting
npm ERR! 403 a package version that is forbidden by your security policy, or
npm ERR! 403 on a server you do not have access to.
npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2022-05-30T23_40_02_692Z-debug-0.log

“docker_build_main” which builds and pushes the container image worked just fine


I see it pushed up to


which I can easily pull down locally

$ docker pull
latest: Pulling from isaac.johnson/dockerwithtests2
e4d61adff207: Already exists
4ff1945c672b: Already exists
ff5b10aec998: Already exists
12de8c754e45: Already exists
ada1762e7602: Already exists
6d1aaa85aab9: Already exists
a238e70d0a8a: Already exists
a9d886ece6c9: Already exists
a213b9afda04: Already exists
d387f0860b3d: Pull complete
d9314303a3e0: Pull complete
3d6190d543f7: Pull complete
6280f0771eac: Pull complete
0d8e113e94ff: Pull complete
Digest: sha256:4920dd293ff0b1886936faabda2d5ab611287c0417e059735c0401a786774f82
Status: Downloaded newer image for

The dockerfile runs tests as we can see

$ cat Dockerfile
FROM node:17.6.0 as base

# simple comment

COPY package.json package.json
COPY package-lock.json package-lock.json

FROM base as test
RUN npm ci
COPY . .
RUN npm run test

FROM base as prod
ENV NODE_ENV=production
RUN npm ci --production
COPY . .
CMD [ "node", "server.js" ]

We can see the results of the Docker tests via Mocha in the logs



In this public GH repo for Containerized COBOL we can see actions that have been run


Visually we can see an invokation as chained steps


The YAML file is similar

$ cat .github/workflows/primary.yml
name: PR And Main Build
      - main

    runs-on: ubuntu-latest
      - name: Check out repository code
        uses: actions/checkout@v2
      - name: Turn on build light
        run: |
          curl -s -o /dev/null
      - name: Build Dockerfile
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          docker build -t $BUILDIMGTAG .
          docker images
      - name: "Dockerhub: Tag and Push"
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          export FINALVERSION="`cat Dockerfile | tail -n1 | sed 's/^.*://g'`"
          docker tag $BUILDIMGTAG idjohnson/coboladder:$FINALVERSION
          docker images
          docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"

          docker push idjohnson/coboladder:$FINALVERSION
        env: # Or as an environment variable
          DOCKER_PASSWORD: $
          DOCKER_USERNAME: $
      - name: "HarborCR: Tag and Push"
        run: |
          export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
          export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"
          docker tag $BUILDIMGTAG $FINALBUILDTAG
          docker images
          echo $CR_PAT | docker login -u $CR_USER --password-stdin
          docker push $FINALBUILDTAG
        env: # Or as an environment variable
          CR_PAT: $
          CR_USER: $
      - name: Build count
        uses: masci/datadog@v1
          api-key: $
          metrics: |
            - type: "count"
              name: "cobol.runs.count"
              value: 1.0
              host: $
                - "project:$"
                - "branch:$"
      - name: Turn off build light
        run: |
          curl -s -o /dev/null

    runs-on: ubuntu-latest
    needs: [HostedActions]
    if: always() && (needs.HostedActions.result == 'failure')
      - name: Blink and Leave On build light
        run: |
          curl -s -o /dev/null && sleep 5 \
          && curl -s -o /dev/null && sleep 5 \
          &&  curl -s -o /dev/null && sleep 5 \
          && curl -s -o /dev/null
      - name: Fail count
        uses: masci/datadog@v1
          api-key: $
          metrics: |
            - type: "count"
              name: "cobol.fails.count"
              value: 1.0
              host: $
                - "project:$"
                - "branch:$"

Unlike Gitlab, however, the logs are part of the job and after enough time, are removed


However, if I look at a more current Action run


I can view and expand logs from the run


Container Images

I showed earlier Gitlab’s CR. Github has one as well.

If I add a step to the Containerized COBOL run

      - name: "GHCR: Tag and Push"
        run: |
           export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
           export FINALVERSION="`cat Dockerfile | tail -n1 | sed 's/^.*://g'`"
           echo $GHPAT | docker login -u idjohnson --password-stdin
           docker tag $BUILDIMGTAG$FINALVERSION
           docker images
           docker push$FINALVERSION
          GHPAT: $

This should trigger a build when committed to main


And after fighting the GH PAT, I got it to work (lesson learned is fine-grained repo bound PATs do not work with ghcr uploads)



We can see build output in Gitea, but like Github, after a while the logs expire


Since I want to build a container image, I’ll install a Gitea agent on a VM with docker

builder@builder-T100:~/GiteaRunner$ wget
--2023-08-15 06:55:59--
Resolving (,,, ...
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 17784832 (17M) [application/octet-stream]
Saving to: ‘act_runner-0.2.5-linux-amd64’

act_runner-0.2.5-linux-amd64        100%[==================================================================>]  16.96M  58.8MB/s    in 0.3s

2023-08-15 06:56:00 (58.8 MB/s) - ‘act_runner-0.2.5-linux-amd64’ saved [17784832/17784832]

builder@builder-T100:~/GiteaRunner$ chmod 755 act_runner-0.2.5-linux-amd64
builder@builder-T100:~/GiteaRunner$ ./act_runner-0.2.5-linux-amd64 register
INFO Registering runner, arch=amd64, os=linux, version=v0.2.5.
WARN Runner in user-mode.
INFO Enter the Gitea instance URL (for example,
INFO Enter the runner token:
INFO Enter the runner name (if set empty, use hostname: builder-T100):

INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):

INFO Registering runner, name=builder-T100, instance=, labels=[ubuntu-latest:docker://node:16-bullseye ubuntu-22.04:docker://node:16-bullseye ubuntu-20.04:docker://node:16-bullseye ubuntu-18.04:docker://node:16-buster].
DEBU Successfully pinged the Gitea instance server
INFO Runner registered successfully.
builder@builder-T100:~/GiteaRunner$ ./act_runner-0.2.5-linux-amd64 daemon
INFO[2023-08-15T06:57:56-05:00] Starting runner daemon
WARN[2023-08-15T06:57:57-05:00] Because the Gitea instance is an old version, skip declare labels and version.

Following steps to install, I can now see the runner


When that failed, as it pulls an ubuntu image without docker


I tried running the agent as a docker image

$ docker run \
    -v /home/builder/GiteaRunner/config.yaml:/config.yaml \
    -v /home/builder/GiteaRunner/data:/data \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -e CONFIG_FILE=/config.yaml \
    -e GITEA_RUNNER_REGISTRATION_TOKEN=U**********************************S \
    -e GITEA_RUNNER_NAME=buildert100 \
    -e GITEA_RUNNER_LABELS=onprem \
    --name buildert100 \
    -d gitea/act_runner:nightly


That didn’t get much farther. I tried changing lables to use “:host”, but then the job hung


with no real logs

builder@builder-T100:~/GiteaRunner$ docker logs buildert100
time="2023-08-15T12:39:12Z" level=info msg="Starting runner daemon"
time="2023-08-15T12:39:12Z" level=warning msg="Because the Gitea instance is an old version, skip declare labels and version."

builder@builder-T100:~/GiteaRunner$ docker exec -it buildert100 /bin/bash
4a3703b55914:/# ls /var/lo
local/ lock/  log/
4a3703b55914:/# ls /var/log/
4a3703b55914:/# ps -ef
    1 root      0:00 /sbin/tini -- /opt/act/
    7 root      0:00 bash /opt/act/
    8 root      0:00 act_runner daemon --config /config.yaml
   18 root      0:00 /bin/bash
   25 root      0:00 ps -ef
4a3703b55914:/# cd ~
4a3703b55914:~# ls
4a3703b55914:~# ls -ltra
total 12
drwxr-xr-x    1 root     root          4096 Aug 15 12:39 ..
drwxr-xr-x    3 root     root          4096 Aug 15 12:39 .cache
drwx------    1 root     root          4096 Aug 15 12:39 .

I took some time and finally figured it out.

A label needs to have a “name” and a “host”. By default, it seems to want to use Node-16:buster in docker.

When I added “onprem”, but didn’t give a host or container image, it just dropped to default. When i looked at the “.runner”, it was set to use Node-16 images for all labels.

I changed “.runner”

builder@builder-T100:~/GiteaRunner$ cat .runner
  "WARNING": "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner.",
  "id": 3,
  "uuid": "1e6c59d9-9e8c-4e74-a91a-48c8ebcc9e57",
  "name": "builder-T100",
  "token": "4ce3850e51350f4022b4a936d9b5197467d2c7e6",
  "address": "",
  "labels": [

When fixed, I could see the runner work in daemon mode

[Gitea Actions Dockerfile/Explore-Gitea-Actions] [DEBUG] Wrote command

cd /home/builder/.cache/act/a978fd83372cebdb/hostexecutor/AgentBuild
/usr/bin/docker build -t azdoagent:0.0.2 .
/usr/bin/docker images

 to 'workflow/8'
[+] Building 77.8s (17/17) FINISHED
|  => [internal] load .dockerignore                                          0.0s
|  => => transferring context: 2B                                            0.0s
|  => [internal] load build definition from Dockerfile                       0.0s
|  => => transferring dockerfile: 2.88kB                                     0.0s
|  => [internal] load metadata for            1.0s
|  => [ 1/12] FROM  1.7s
|  => => resolve  0.0s
|  => => sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea0 1.13kB / 1.13kB  0.0s
|  => => sha256:3246518d9735254519e1b2ff35f95686e4a5011c90c8534 424B / 424B  0.0s
|  => => sha256:6df89402372646d400cf092016c28066391a26f5d46 2.30kB / 2.30kB  0.0s
|  => => sha256:edaedc954fb53f42a7754a6e2d1b57f091bc9b110 27.51MB / 27.51MB  0.7s
|  => => extracting sha256:edaedc954fb53f42a7754a6e2d1b57f091bc9b11063bc445  0.8s
|  => [internal] load build context                                          0.0s
|  => => transferring context: 2.60kB                                        0.0s
|  => [ 2/12] RUN DEBIAN_FRONTEND=noninteractive apt-get update              3.5s
|  => [ 3/12] RUN DEBIAN_FRONTEND=noninteractive apt-get upgrade -y          1.5s
|  => [ 4/12] RUN DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --  30.2s
|  => [ 5/12] RUN DEBIAN_FRONTEND=noninteractive apt update     && DEBIAN_F  6.6s
|  => [ 6/12] RUN set -eux;   arch="$(uname -m)";  case "$arch" in   x86_64  4.5s
|  => [ 7/12] RUN curl -L "  1.7s
|  => [ 8/12] RUN ln -s /usr/local/bin/docker-compose /usr/local/lib/docker  0.3s
|  => [ 9/12] RUN curl -sL | bash         19.3s
|  => [10/12] WORKDIR /azp                                                   0.0s
|  => [11/12] COPY ./ .                                              0.0s
|  => [12/12] RUN chmod +x                                          0.3s
|  => exporting to image                                                     7.0s
|  => => exporting layers                                                    7.0s
|  => => writing image sha256:a5afc63a0a519d85f5817b3384d2dca680c337bfedd4c  0.0s
|  => => naming to                         0.0s
| REPOSITORY             TAG           IMAGE ID       CREATED         SIZE
| azdoagent              0.0.2         a5afc63a0a51   8 seconds ago   1.23GB
| gitea/act_runner       nightly       ba7fc78926e0   3 days ago      37.5MB
| node                   16-bullseye   cf2eae8774a2   4 days ago      940MB
| louislam/uptime-kuma   1             9a2976449ae2   3 months ago    483MB
| rundeck/rundeck        3.4.6         4969c5ffbb8b   15 months ago   736MB
[Gitea Actions Dockerfile/Explore-Gitea-Actions]   ✅  Success - Main cd /home/builder/.cache/act/a978fd83372cebdb/hostexecutor/AgentBuild
/usr/bin/docker build -t azdoagent:0.0.2 .
/usr/bin/docker images
[Gitea Actions Dockerfile/Explore-Gitea-Actions] [DEBUG] expression 'cd $/Age


And see the image in the log


There are OCI container image artifacts in Gitea, but we’ll explore those at a later date - but suffice to say it is possible.



One of the newer features is a built-in Validate button


Thus, i

f I make a typo


It can detect it


However, it didn’t really point to the correct line.

When the syntax is right, I can click validate and see a general idea what might happen



Gitea doesn’t really have a YAML validator built-in



Github doesn’t have a built-in validator either, however it does have some Marketplace


and Documentation right-pane tips


Pipeline Visualization

Pipeline Visualization is a unique feature of Gitlab Pipelines

Here I can see the stages and how they might line up




There are basic artifacts as well. By default, the pipeline logs end up in Artifacts



Github has packages as well, including containers


Here we can see the container package as built earlier


with details


It seems that by default the packages are private. We can change that though per package


I tested a pull from an un-authed machine and it worked. I’m sure there are rate limits or something - Github likely isn’t making this free without limits. I believe the billing of artifacts is part of your packages plan

builder@builder-T100:~/GiteaRunner$ docker pull
0.0.3: Pulling from idjohnson/coboladder
a1d0c7532777: Pull complete
4f4fb700ef54: Pull complete
a6b39a810ad4: Pull complete
6ea58972a31d: Pull complete
9c016aa06e8a: Pull complete
7a94e52779b6: Pull complete
d4cae95afecc: Pull complete
2063917dd998: Pull complete
3157baffbd2f: Pull complete
655e107b5aec: Pull complete
4144dc00dadb: Pull complete
6494473a3182: Pull complete
07f813345ff3: Pull complete
0461a252b5fe: Pull complete
320450687e1d: Pull complete
fcb189e5e315: Pull complete
d3e991c5cf55: Pull complete
299b5fe598df: Pull complete
cc31d201c6be: Pull complete
f5164734ffeb: Pull complete
Digest: sha256:882705b34d7a7fcb47b4ac2a1155fae2b07b4cc0307d252f5b31d0731b3afafb
Status: Downloaded newer image for

Scheduled Pipelines


We can schedule pipelines in a different area, “Schedules”


For instance, I could create a schedule at 2AM daily


And see it scheduled about 6 hours from now



Like Gitea, Github has schedule triggers


I realized I was expecting to set an always flag like AzDO but that isn’t needed with Github

Azure DevOps Syntax:

- cron: string # cron syntax defining a schedule
  displayName: string # friendly name given to a specific schedule
    include: [ string ] # which branches the schedule applies to
    exclude: [ string ] # which branches to exclude from the schedule
  always: boolean # whether to always run the pipeline or only if there have been source code changes since the last successful scheduled run. The default is false.
  batch: boolean # Whether to run the pipeline if the previously scheduled run is in-progress; the default is false.
  # batch is available in Azure DevOps Server 2022.1 and higher

After a few hours, I could see many 15m seperated builds had occurred


And we can also see that the “event” that triggered a build was a cron schedule (versus a push)



Theoretically, it should follow the same format as Github

However, I changed it to use an every 15m schedule


And at least as of my instance (1.19.3), it didn’t trigger



Github has a pretty generous free tier. They charge for more minutes of private builds and their AI (Github CoPilot).


These limits mostly apply to their host agents. Unlike AzDO that charges you for additional private agents, Github just has some (sane, in my opinion) limits on private agents. Namely, workflows limited to 35 days and 10k self-hosted runners in a Runner Group (pool).


Gitlab pricing is very similar in structure


Though, if we compare on a few similar features

Company Product price/user/month CICD minutes Package Storage security scanning Users in group
Github Free 0 2000 500Mb - unlimited
Gitlab Free 0 400 5Gb - 5
Github Team 4 3000 2Gb protected branches unlimited
Gitlab Premium 29 10k 50Gb protected branches -
Github Enterprise 21 50k 50Gb SCO1, 2, FedRamb, Adv Audition, Env protections, etc -
Gitlab Ultimate 99 50k 250Gb Dynamic App Sec, Sec Dashboards, Static App Security Testing, etc Free Guest Users

Gitea is MIT and OS. You host it. That said, if you want support, they have various support options


They range from 20 users to 10k users, but the rate is flat - US$19/user/month for support.


This is a hard comparison but let us review three modes we might be considering:

The Enthusiast/Blogger/Student/Individual

Like me, perhaps you are an army of one and just need a system to handle personal needs.

All three have free options.

  1. Github: We get quite a lot here. If you make your project public/Open Source you have nearly unlimited usage of agents/etc. This is really generous, provided you want to share. Even the free tier of private agents and repos is generous. It takes a while to burn through your private repo Actions limits (which I did once and had to pay for a month when I used to build this with Ghost instead of Jekyll)
  2. Gitlab: The limits on runners is a lot less. To be fair, it’s not like Gitlab owns a cloud like Github does (with Microsoft). I also find some really awesome features unique to Gitlab - like tight JIRA integrations and Pipeline visualizations. I would have no troubles living within the limits on the free tier here
  3. Gitea: You self-host - there are no limits - but it’s not like electricity to my home is free (okay, with Solar, it’s damn near free - but I’m still paying those panels off). I find Gitea is a bit more jenky - that is, there are nuances between versions and the self hosted Runner to some finagling. I still plan to lean into this.

There is some real value, in my opinion with Github. As the big dog in the game, there are lots of guides, tutorials and examples out there. Plenty of ways to solve issues. In much the same way I’ve leaned into Ansible, not because I think Ansible is the greatest (I rather like Puppet and Chef), but it’s the most popular and easy to find example playbooks.

Small Business/Teams/Startups

There are needs for startups that really make Github and Gitea the leading options. If I had between 10 and 20 users, I can get Teams version of Github for US$40-80 a month compared to the #29/user/month for Gitlab that has to be paid for a year in advance. That means for 20-40 users you are out US$6,960 to US$13,920 right off the bat.

You do get a lot more there (50Gb storage/100Gb transfer) versus Github’s 2Gb package storage.

However, It would just seem obvious to stick either to the OS Gitea and once we are underway, think about paying $4560-$9120 for support - that includes upgrade service and telephone/remote support.


Let’s consider a situation of 200-500 developers

  • Github: US$50,400 - US$126,000 ($21/u/mo)
    • Pretty good storage, not as much as Gitlab
  • Gitlab: US$237,600 - US$594,000 ($99/u/mo)
    • Test Suites
    • Lots of storage
    • Full Project Management
  • Gitea: US$45,600 - US$114,00 ($19/u/mo)
    • but you have to handle your own Infrastructure and Data Centers are not cheap

If we are looking to use Github, I would argue you likely need some kind of professional Issue management. The Issue management in Github/Gitea is pretty basic - works for engineers, but doesn’t handle proper agile flows with Epics, Stories and Requirements

This means we either get the $7.75/u/mo “standard” JIRA or $15.25/mo “premium” user. Assuming we are looking for professional, that would be: 200 users (13.15) - US$2630*12 = US$31,560/year, or paid year upfront US$26,500 and $5,107.50 (10.22) * 12 = $61,290/year or $51,000 paid up front. (you have to play with the JIRA Pricing Calculator)

So that might mean we spend US$126,000 for Github, but then have to add in $51,000 for JIRA = $177,000. Now we still have to think about test suites to pair with that if we cannot containerize (such as LambdaTest). It can add up (and I might very well start to revisit Azure DevOps if I needed a full suite).

There can be other considerations as well. One could compete with Microsoft and desire to not use Github/Azure as a result. I have seen similar things with companies in Minnesota that compete with Amazon and desire to not use AWS as a result (Best Buy, Target, etc).

There is a great lengthy article on Gitlab pricing on Gitlab’s site. However, it doesn’t really explain or justify the much higher costs than it’s chief competitor (Github).

Azure DevOps

I don’t want to exclude many other great tools.

For instance, Azure DevOps has Repos, Pipelines and Artifacts. It’s per-seat cost is fairly cheap ($6/mo/user and free “stakeholder” seats), but the pricing looses its luster in Pipeline agent costs:

  • MS Hosted Agent: $40/mo/per parellel job
  • Self-hosted: $15/mo/per agent (yes, you pay $15/mo to host your own!)
  • storage - up to 2Gb of artifact storage is free, but, for instance, 20Gb/mo would be $26/mo
  • If you want Test Plan Management, its $52/user/mo


Github Gitlab Gitea

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes