Skip to content
Notifications
Clear all

Comparison: GitHub Actions vs GitLab CI for a team heavy on container builds.

4 Posts
4 Users
0 Reactions
2 Views
(@harperk)
Reputable Member
Joined: 1 week ago
Posts: 144
Topic starter   [#9554]

We just finished a two-month slog migrating from GitLab CI to GitHub Actions. The trigger was a company acquisition and the new overlords' "platform standardization" decree. Since we spend roughly 70% of our pipeline cycles building, testing, and pushing multi-architecture container images, this was less about simple job translation and more about re-engineering the entire delivery spine.

GitLab's integrated container registry with its clean project-to-image relationship was a genuine asset we took for granted. Moving to GitHub Container Registry (GHCR) meant revisoring every image pull reference and overhauling our cleanup policies. The pipeline syntax translation itself was mostly tedious find-and-replace, but the devil was in the caching and layer reuse. GitLab's cache keys tied to Docker layers felt more intuitive for our use case. In Actions, we spent a solid week tweaking the `docker/build-push-action` with custom cache-from and cache-to parameters to get comparable build times. The "matrix" strategy for multi-arch builds is more verbose but ultimately more explicit, which I grudgingly appreciate.

The real pain point was secrets migration and the permission model. GitLab's group-level variables and protected branches offered a granularity that Actions' organization secrets and environments are still catching up to. We ended up with a slightly terrifying GitHub Personal Access Token (classic) as a repository secret to manage GHCR deletions because the default GITHUB_TOKEN permissions are, frankly, anemic for our old workflows. Monitoring is also a step back; GitLab's pipeline analytics gave us a clearer view of resource consumption and bottlenecks than Actions' current offerings.

So, for container-heavy teams: if you're deeply nested in the GitLab ecosystem, the move to Actions is a lateral one with significant operational tax. The compute might be cheaper, but you'll pay for it in configuration complexity and missing observability. Would I do it again voluntarily? Not a chance. But now that it's done, it works. Mostly.


Data over dogma.


   
Quote
(@cloud_rookie_em)
Estimable Member
Joined: 3 months ago
Posts: 138
 

Junior platform engineer here at a mid-sized e-commerce company. We run about 30 microservices on AWS ECS and moved from Jenkins to self-managed GitLab CI last year. Our pipelines are 80% container builds and pushes to ECR.

1. **Container registry integration** - GitLab's is built-in and automatic per project. With GitHub, you must explicitly configure GHCR and manage cleanup with third-party actions or API calls.
2. **Pipeline configuration model** - GitLab's `.gitlab-ci.yml` with its `services:` keyword for DinD felt simpler for container builds. GitHub's action-based, marketplace-centric model offers more choice but demands more integration glue.
3. **Cache granularity for Docker** - GitLab's cache key tied directly to Dockerfile content. In Actions, we had to script layer caching ourselves using `docker/build-push-action` `cache-from:` and commit SHA, adding ~15 lines per job.
4. **Permission boundaries** - GitLab's project-level CI/CD variables and protected tags were clearer for us. GitHub's secrets and organization/project levels required careful auditing post-migration to avoid over-permissive workflows.

I'd stick with GitLab CI for a container-heavy team, purely for the integrated registry and simpler caching. If you're already on GitHub for code and need unified tooling, GitHub Actions can work but needs a dedicated week to tune caching and image cleanup.



   
ReplyQuote
(@joshuae)
Trusted Member
Joined: 1 week ago
Posts: 47
 

The cache-from and cache-to parameter tweaking is a notorious time sink. We documented our final configuration as a composite action because the matrix strategy for multi-arch complicates cache targeting. You need a separate cache key per architecture, but you also want some shared layers. Our solution was to use a dedicated cache backend (a GCS bucket) instead of the GitHub Actions cache, which gave us finer control over retention and avoided the 10GB per repo limit.

The permission model shift from GitLab to GitHub was the most significant architectural adjustment for us, too. GitLab's project-level runners with their static IAM roles simplified pushing to our cloud registry. With GitHub, the granularity of GITHUB_TOKEN permissions and the dance required to assume a cloud role via OIDC felt more secure but introduced new failure modes, especially for container pushes from forks.

Your point about the matrix strategy being more explicit is correct, but that verbosity becomes a maintenance burden across dozens of services. We ended up building a lightweight generator for our workflows to keep the DRY principle intact.


Latency is the enemy


   
ReplyQuote
(@billyp)
Estimable Member
Joined: 6 days ago
Posts: 59
 

Spot on about the external cache backend - we went with an S3 bucket for the same reason. The 10GB limit evaporates and you can set lifecycle rules to automatically prune old caches, something GH Actions cache doesn't do.

That permission dance with OIDC is real. We wrote a small custom action just to assume the AWS role and output temporary credentials as environment variables. It cut down the boilerplate in every workflow file, but yeah, it's an extra layer GitLab users don't think about.

The generator approach is smart. We used a similar pattern: a central "build-push" composite action that all our service repos call. It handles the multi-arch matrix and cache-to/cache-from mess in one place. DRY saves the day when you're managing 50+ image builds.


Always A/B test.


   
ReplyQuote