[{"data":1,"prerenderedAt":708},["ShallowReactive",2],{"/en-us/blog/scaling-down-how-we-prototyped-an-image-scaler-at-gitlab/":3,"navigation-en-us":36,"banner-en-us":453,"footer-en-us":468,"Matthias Käppler":679,"next-steps-en-us":693},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":26,"_id":29,"_type":30,"title":31,"_source":32,"_file":33,"_stem":34,"_extension":35},"/en-us/blog/scaling-down-how-we-prototyped-an-image-scaler-at-gitlab","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"Scaling down: How we shrank image transfers by 93%","Our approach to delivering an image scaling solution to speed up GitLab site rendering","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749664102/Blog/Hero%20Images/gitlab-values-cover.png","https://about.gitlab.com/blog/scaling-down-how-we-prototyped-an-image-scaler-at-gitlab","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"Scaling down: How we shrank image transfers by 93%\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Matthias Käppler\"}],\n        \"datePublished\": \"2020-11-02\",\n      }",{"title":9,"description":10,"authors":17,"heroImage":11,"date":19,"body":20,"category":21,"tags":22},[18],"Matthias Käppler","2020-11-02","\n\n{::options parse_block_html=\"true\" /}\n\n\n\nThe [Memory](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/application_performance/) team recently shipped an improvement to our image delivery functions\nthat drastically reduces the amount of data we serve to clients. Learn here how we went from knowing nothing about\n[Golang](https://golang.org/) and image scaling to a working on-the-fly image scaling solution built into \n[Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse).\n\n## Introduction\n\nImages are an integral part of GitLab. Whether it is user and project avatars, or images embedded in issues\nand comments, you will rarely load a GitLab page that does not include images in some way shape or form.\nWhat you may not be aware of is that despite most of these images appearing fairly small when presented\non the site, until recently we were always serving them in their original size.\nThis meant that if you would visit a merge request, then all user avatars that appeared merely as thumbnails\nin sidebars or comments would be delivered by the GitLab application in the same size they were uploaded in,\nleaving it to the browser rendering engine to scale them down as necessary. This meant serving\nmegabytes of image data in a single page load, just so the frontend would throw most of it away!\n\nWhile this approach was simple and served us well for a while, it had several major drawbacks:\n\n- **Perceived latency suffers.** The perceived latency is the time that passes between a user\n  requesting content, and that content actually becoming visible or being ready to engage with.\n  If the browser has to download several megabytes of image data, and then has to furthermore\n  scale down those images to fit the cells they are rendered into, the user experience unnecessarily suffers.\n- **Egress traffic cost.** On gitlab.com, we store all images in object storage, specifically GCS\n  (Google Cloud Storage). This means that our Rails app first needs to resolve an image entity to\n  a GCS bucket URL where the binary data resides, and have the client\n  download the image through that endpoint. This means that for every image served, we cause\n  traffic from GCS to the user that we have to pay for, and the more data we serve, the higher the cost.\n\nWe therefore took on the challenge to both improve rendering performance and reduce traffic costs\nby implementing [an image scaler that would downscale images](https://gitlab.com/groups/gitlab-org/-/epics/3822)\nto a requested size before delivering them to the client.\n\n### Phase 1: Understanding the problem\n\nThe first problem is always: understand the problem! What is the status quo exactly? How does it work?\nWhat is broken about it? What should we focus on?\n\nWe had a pretty good idea of the severity of the problem, since we regularly run performance tests\nthrough [sitespeed.io](https://www.sitepeed.io) that highlight performance problems on our site.\nIt had identified images sizes as one of the most severe issues:\n\n![sitespeed performance test](https://gitlab.com/groups/gitlab-org/-/uploads/a06d8bfde802995c577afca843be7e96/Bildschirmfoto_2020-07-15_um_11.45.44.png)\n\nTo better inform a possible solution, an essential step was to [collect enough data](https://gitlab.com/gitlab-org/gitlab/-/issues/227387)\nto help identify the areas we should focus on. Here are some of the highlights:\n\n- **Most images requested are avatars.** We looked at the distribution of requests for certain types of images.\n  We found that about 70% of them were for avatars, while the remaining 30% accounted for embedded images.\n  This suggested that any solution would have the biggest reach if we focused on avatars first. Within the\n  avatar cohort we found that about 62% are user avatars, 22% are project avatars, and 16% are group avatars,\n  which isn't surprising. \n- **Most avatars requested are PNGs or JPEGs.** We also looked at the distribution of image formats. This is partially\n  affected by our upload pipeline and how images are processed (for instance, we always crop user avatars and store them as PNGs)\n  but we were still surprised to see that both formats made up 99% of our avatars (PNGs 76%, JPEGs 23%). Not much\n  love for GIFs here!\n- **We serve 6GB of avatars in a typical hour.** Looking at a representative window of 1 hour of GitLab traffic, we saw\n  almost 6GB of data move over the wire, or 144GB a day. Based on experiments with downscaling a representative user avatar,\n  we estimated that we could reduce this to a mere 13GB a day on average, saving 130GB of bandwidth each day!\n\nThis was proof enough for us that there were significant gains to be made here. Our first intuition was: could this\nbe done by a CDN? Some modern CDNs like Cloudflare [already support image resizing](https://support.cloudflare.com/hc/en-us/articles/360028146432-Understanding-Cloudflare-Image-Resizing)\nin some of their plans. However, we had two major concerns about this:\n\n1. **Supporting our self-managed customers.** While gitlab.com is the largest GitLab deployment we know of, we have hundreds of thousands\n  of customers who run their own GitLab installation. If we were to only resize images that pass through a CDN in front of gitlab.com,\n  none of those customers would benefit from it.\n1. **Pricing woes.** While there are request budgets based on your CDN plan, we were worried about the operational cost this would\n  add for us and how to reliably predict it.\n\nWe therefore decided to look for a solution that would work for all GitLab users, and that would be more under\nour own control, which led us to phase 2: experimentation!\n\n### Phase 2: Experiments, experiments, experiments!\n\nA frequent challenge for [our team (Memory)](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/application_performance/)\nis that we need to venture into parts of GitLab's code base\nthat we are unfamiliar with, be it with the technology, the product area, or both. This was true in this\ncase as well. While some of us had some exposure to image scaling services, none of us had ever built or\nintegrated one.\n\nOur main goal in phase 2 was therefore to identify what the possible approaches to image scaling were,\nexplore them by researching existing solutions or even building proof-of-concepts (POCs), and grade\nthem based on our findings. The questions we asked ourselves along the way were:\n\n- **When should we scale?** Upfront during upload or on-the-fly when an image is requested?\n- **Who does the work?** Will it be a dedicated service? Can it happen asynchronously in Sidekiq?\n- **How complex is it?** Whether it's an existing service we integrate, or something we build ourselves,\n  does implementation or integration complexity justify its relatively simple function?\n- **How fast is it?** We shouldn't forget that we set out to solve a performance issue. Are we sure that\n  we are not making the server slower by the same amount of time we save in the client?\n\nWith this in mind, we identifed [multiple architectural approaches](https://gitlab.com/groups/gitlab-org/-/epics/3979) to consider,\neach with their own pros and cons. These issues also doubled as a form of [architectural decision log](https://github.com/joelparkerhenderson/architecture_decision_record#what-is-an-architecture-decision-record)\nso that decisions for or against an approach are recorded.\n\nThe major approaches we considered are outlined next.\n\n#### Static vs. dynamic scaling\n\nThere are two basic ways in which an image scaler can operate: it can either create thumbnails of\nan existing image ahead of time, e.g. during the original upload as a background job. Or it can\nperform that work on demand, every time an image is requested. To make a long story short: while\nit took a lot of back and forth, and even though we had [a working POC](https://gitlab.com/gitlab-org/gitlab/-/issues/232616),\nwe eventually discarded the idea of scaling statically, at\nleast for avatars. Even though [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) (the Ruby uploader\nwe employ) has an integration\nwith MiniMagick and is able to perform that kind of work, it suffered from several issues:\n\n1. **Maintenance heavy.** Since image sizes may change over time, a strategy is needed to backfill sizes\n  that haven't been computed yet. This raised questions especially for self-managed customers where\n  we do not control the GitLab installation.\n1. **Statefulness.** Since thumbnails are created alongside the original image, it was unclear how to perform\n  cleanups should they become necessary, since CarrierWave does not store these as separate database\n  entities that we could easily query.\n1. **Complexity.** The POC we created turned out to be more complex than anticipated and felt like we\n  were shoehorning this feature onto existing code. This was exacerbated by the fact that at the time\n  we were running a very old version of CarrierWave that was already a maintenance liability, and upgrading it\n  would have added scope creep and delays to an already complex issue.\n1. **Flexibility.** The actual scaler implementation in CarrierWave is buried three layers down the Ruby dependency stack,\n  and it was difficult to replace the actual scaler binary (which would become a\n  problem when trying to secure this solution as we will see in a moment.)\n\nFor these reasons we decided to scale images on-the-fly instead.\n\n### Dynamic scaling: Workhorse vs. dedicated proxy\n\nWhen scaling images on-the-fly the question becomes: where? Early on there was a suggestion to use\n[imgproxy](https://github.com/imgproxy/imgproxy), a \"fast and secure standalone server for resizing and converting remote images\".\nThis sounded tempting, since it is a \"batteries included\" offering, it's free to use, and it is a great\nway to isolate the task of image scaling from other production work loads, which has benefits around\nsecurity and fault isolation.\n\nThe main problem with imgproxy was exactly that, however: a standalone server. \n[Introducing a new service to GitLab](https://docs.gitlab.com/ee/development/adding_service_component.html#adding-a-new-service-component-to-gitlab)\nis a complex task, since we strive to appear as a [single application](https://about.gitlab.com/handbook/product/single-application/) to the end user,\nand documenting, packaging, configuring, running and monitoring a new service just for rescaling images seemed excessive.\nIt therefore wasn't in line with our prerogative of focusing on the\n[minimum viable change](https://about.gitlab.com/handbook/product/product-principles/#the-minimal-viable-change-mvc).\nMoreover, imgproxy had significant overlap with existing architectural components at GitLab, since we already\nrun a reverse proxy: [Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse).\n\nWe therefore decided that the fastest way to deliver an MVC was to build out this functionality in Workhorse\nitself. Fortunately we found that we already had an established pattern for dealing with special, performance\nsensitive workloads, which meant that we could\nlearn from existing solutions for similar problems (such as image delivery from remote storage), and we could\nlean on its existing integration with the Rails application for request authentication and running business\nlogic such as validating user inputs, which helped us tremendously to focus on the actual problem: scaling images.\n\nThere was a final decision to make, however: scaling images is a very different kind of workload from \nserving ordinary requests, so an open question was how to integrate a scaler into Workhorse in a way\nthat would not have knock-on effects on other tasks Workhorse processes need to execute.\nThe two competing approaches discussed were to either shell out to an executable that performs the scaling,\nor run a [sidecar process](https://docs.microsoft.com/en-us/azure/architecture/patterns/sidecar#:~:text=Sidecars%20are%20supporting%20processes%20or,fate%20of%20its%20parent%20application.)\nthat would take over image scaling work loads from the main Workhorse process.\n\n### Dynamic scaling: Sidecar vs. fork-on-request\n\nThe main benefit of a sidecar process is that it has its own life-cycle and memory space, so it can be tuned\nseparately from the main serving process, which improves fault isolation. Moreover, you only pay the\ncost for starting the process once. However, it also comes with\nadditional overhead: if the sidecar dies, something has to restart it, so we would have to look at\nprocess supervisors such as `runit` to do this for us, which again comes with a significant amount\nof configuration overhead. Since at this point we weren't even sure how costly it would be to serve\nimage scaling requests, we let our MVC principle guide us and decided to first explore the simpler\nfork-on-request approach, which meant shelling out to a dedicated scaler binary on each image scaling\nrequest, and only consider a sidecar as a possible future iteration.\n\nForking on request was [explored as a POC](https://gitlab.com/gitlab-org/gitlab/-/issues/230519)\nfirst, and was quickly made production ready and deployed\nbehind a feature toggle. We initially ended up settling on [GraphicsMagick](http://www.graphicsmagick.org/)\nand its `gm` binary to perform the actual image scaling for us, both because it is a battle tested library, but also\nbecause there was precedent at GitLab to use it for existing features, which allowed us to ship\na solution even faster.\n\nThe overall request flow finally looked as follows:\n\n```mermaid\nsequenceDiagram\n    Client->>+Workhorse: GET /image?width=64\n    Workhorse->>+Rails: forward request\n    Rails->>+Rails: validate request\n    Rails->>+Rails: resolve image location\n    Rails-->>-Workhorse: Gitlab-Workhorse-Send-Data: send-scaled-image\n    Workhorse->>+Workhorse: invoke image scaler\n    Workhorse-->>-Client: 200 OK\n```\n\nThe \"secret sauce\" here is the `Gitlab-Workhorse-Send-Data` header synthesized by Rails. It carries\nall necessary parameters for Workhorse to act on the image, so that we can maintain a clean separation\nbetween application logic (Rails) and serving logic (Workhorse).\nWe were fairly happy with this solution in terms of simplicity and ease of maintenance, but we\nstill had to verify whether it met our expectations for performance and security.\n\n### Phase 3: Measuring and securing the solution\n\nDuring the entire development cycle, we frequently measured the performance of the various appraoches\nwe tested, so as to understand how they would affect request latency and memory use.\nFor latency tests we relied on [Apache Bench](https://httpd.apache.org/docs/2.4/programs/ab.html), since\nrecalling our initial mission, we were mostly interested in reducing the request latency a user might experience.\n\nWe also ran benchmarks encoded as Golang tests that specifically [compared different scaler implementations](https://gitlab.com/ayufan/image-resizing-test)\nand how performance changed with different image formats and image sizes. We learned a lot from these\ntests, especially where we would typically lose the most time, which often was in encoding/decoding\nan image, and not in resizing an image per se.\n\nWe also took security very seriously from the start. Some image formats such as SVGs are notorious\nfor remote code execution attacks, but there were other concerns such as DOS-ing the service with\ntoo many scaler requests or PNG compression bombs. We therefore\nput very strict requirements in place around what sizes (both dimensionally but also in bytes) and\nformats we will accept.\n\nUnfortunately one fairly severe issue remained that turned out to be a deal breaker with our simple\nsolution: `gm` is a complex piece of software, and shelling out to a 3rd party binary written in C still\nleaves the door open for a number of security issues. The decision was to [sandbox the binary](https://gitlab.com/groups/gitlab-org/-/epics/4373)\ninstead, but this turned out\nto be a lot more difficult than anticipated. We evaluated but discarded multiple approaches to sandboxing\nsuch as via `setuid`, `chroot` and `nsjail`, as well as building a custom binary on top of [seccomp](https://en.wikipedia.org/wiki/Seccomp).\nHowever, due to performance, complexity or other concerns we discarded all of them in the end.\nWe eventually decided to sarifice some performance for the sake of protecting our users as best we can and \nwrote a scaler binary in Golang, based on an existing [imaging](https://github.com/disintegration/imaging)\nlibrary, which had none of these issues.\n\n### Results, conclusion and outlook\n\nIn roughly two months we took an innocent sounding but in fact complex topic, image scaling, and went\nfrom \"we know nothing about this\" to a fully functional solution that is now running on gitlab.com.\nWe faced many headwinds along the way, in part because we were unfamiliar with both the topic and\nthe technology behind Workhorse (Golang), but also because we underestimated the challenges of delivering\nan image scaler that will be both fast and secure, an often difficult trade-off. A major lesson learned\nfor us is that security cannot be an afterthought; it has to be part of the design from day one and\nmust be part of informing the approach taken.\n\nSo was it a success? Yes! While the feature didn't have as much of an impact on overall perceived client\nlatency as we had hoped, we still dramatically improved a number of metrics. First and foremost, the\ndreaded \"properly size image\" reminder that topped our sitepeed metrics reports is resolved. This is also evident\nin the average image size processed by clients, which for image heavy pages fell off a cliff (that's good -- lower is\nbetter here):\n\n![image size metric](https://gitlab.com/groups/gitlab-org/-/uploads/b453aedaf2132db1292898508fd6a0c1/Bildschirmfoto_2020-10-06_um_07.02.56.png)\n\nSite-wide we saw a staggering **93% reduction** in image transfer size of page content delivered to clients.\nThese gains also translate into savings for GCS egress traffic, and hence Dollar cost savings, by an equivalent amount.\n\nA feature is never done of course, and there are a number of things we are looking to improve in the future:\n\n- Improving metrics and observability\n- Improving performance through more aggressive caching\n- Adding support for WebP and other features such as image blurring\n- Supporting content images embedded into GitLab issues and comments\n\nThe Memory team meanwhile will slowly step back from this work, however, and hand it over to product teams\nas product requirements evolve.\n","unfiltered",[23,24,25],"workflow","testing","performance",{"slug":27,"featured":6,"template":28},"scaling-down-how-we-prototyped-an-image-scaler-at-gitlab","BlogPost","content:en-us:blog:scaling-down-how-we-prototyped-an-image-scaler-at-gitlab.yml","yaml","Scaling Down How We Prototyped An Image Scaler At Gitlab","content","en-us/blog/scaling-down-how-we-prototyped-an-image-scaler-at-gitlab.yml","en-us/blog/scaling-down-how-we-prototyped-an-image-scaler-at-gitlab","yml",{"_path":37,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"data":39,"_id":449,"_type":30,"title":450,"_source":32,"_file":451,"_stem":452,"_extension":35},"/shared/en-us/main-navigation","en-us",{"logo":40,"freeTrial":45,"sales":50,"login":55,"items":60,"search":390,"minimal":421,"duo":440},{"config":41},{"href":42,"dataGaName":43,"dataGaLocation":44},"/","gitlab logo","header",{"text":46,"config":47},"Get free trial",{"href":48,"dataGaName":49,"dataGaLocation":44},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":51,"config":52},"Talk to sales",{"href":53,"dataGaName":54,"dataGaLocation":44},"/sales/","sales",{"text":56,"config":57},"Sign in",{"href":58,"dataGaName":59,"dataGaLocation":44},"https://gitlab.com/users/sign_in/","sign in",[61,105,201,206,311,371],{"text":62,"config":63,"cards":65,"footer":88},"Platform",{"dataNavLevelOne":64},"platform",[66,72,80],{"title":62,"description":67,"link":68},"The most comprehensive AI-powered DevSecOps Platform",{"text":69,"config":70},"Explore our Platform",{"href":71,"dataGaName":64,"dataGaLocation":44},"/platform/",{"title":73,"description":74,"link":75},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":76,"config":77},"Meet GitLab Duo",{"href":78,"dataGaName":79,"dataGaLocation":44},"/gitlab-duo/","gitlab duo ai",{"title":81,"description":82,"link":83},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":84,"config":85},"Learn more",{"href":86,"dataGaName":87,"dataGaLocation":44},"/why-gitlab/","why gitlab",{"title":89,"items":90},"Get started with",[91,96,101],{"text":92,"config":93},"Platform Engineering",{"href":94,"dataGaName":95,"dataGaLocation":44},"/solutions/platform-engineering/","platform engineering",{"text":97,"config":98},"Developer Experience",{"href":99,"dataGaName":100,"dataGaLocation":44},"/developer-experience/","Developer experience",{"text":102,"config":103},"MLOps",{"href":104,"dataGaName":102,"dataGaLocation":44},"/topics/devops/the-role-of-ai-in-devops/",{"text":106,"left":107,"config":108,"link":110,"lists":114,"footer":183},"Product",true,{"dataNavLevelOne":109},"solutions",{"text":111,"config":112},"View all Solutions",{"href":113,"dataGaName":109,"dataGaLocation":44},"/solutions/",[115,140,162],{"title":116,"description":117,"link":118,"items":123},"Automation","CI/CD and automation to accelerate deployment",{"config":119},{"icon":120,"href":121,"dataGaName":122,"dataGaLocation":44},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[124,128,132,136],{"text":125,"config":126},"CI/CD",{"href":127,"dataGaLocation":44,"dataGaName":125},"/solutions/continuous-integration/",{"text":129,"config":130},"AI-Assisted Development",{"href":78,"dataGaLocation":44,"dataGaName":131},"AI assisted development",{"text":133,"config":134},"Source Code Management",{"href":135,"dataGaLocation":44,"dataGaName":133},"/solutions/source-code-management/",{"text":137,"config":138},"Automated Software Delivery",{"href":121,"dataGaLocation":44,"dataGaName":139},"Automated software delivery",{"title":141,"description":142,"link":143,"items":148},"Security","Deliver code faster without compromising security",{"config":144},{"href":145,"dataGaName":146,"dataGaLocation":44,"icon":147},"/solutions/security-compliance/","security and compliance","ShieldCheckLight",[149,152,157],{"text":150,"config":151},"Security & Compliance",{"href":145,"dataGaLocation":44,"dataGaName":150},{"text":153,"config":154},"Software Supply Chain Security",{"href":155,"dataGaLocation":44,"dataGaName":156},"/solutions/supply-chain/","Software supply chain security",{"text":158,"config":159},"Compliance & Governance",{"href":160,"dataGaLocation":44,"dataGaName":161},"/solutions/continuous-software-compliance/","Compliance and governance",{"title":163,"link":164,"items":169},"Measurement",{"config":165},{"icon":166,"href":167,"dataGaName":168,"dataGaLocation":44},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[170,174,178],{"text":171,"config":172},"Visibility & Measurement",{"href":167,"dataGaLocation":44,"dataGaName":173},"Visibility and Measurement",{"text":175,"config":176},"Value Stream Management",{"href":177,"dataGaLocation":44,"dataGaName":175},"/solutions/value-stream-management/",{"text":179,"config":180},"Analytics & Insights",{"href":181,"dataGaLocation":44,"dataGaName":182},"/solutions/analytics-and-insights/","Analytics and insights",{"title":184,"items":185},"GitLab for",[186,191,196],{"text":187,"config":188},"Enterprise",{"href":189,"dataGaLocation":44,"dataGaName":190},"/enterprise/","enterprise",{"text":192,"config":193},"Small Business",{"href":194,"dataGaLocation":44,"dataGaName":195},"/small-business/","small business",{"text":197,"config":198},"Public Sector",{"href":199,"dataGaLocation":44,"dataGaName":200},"/solutions/public-sector/","public sector",{"text":202,"config":203},"Pricing",{"href":204,"dataGaName":205,"dataGaLocation":44,"dataNavLevelOne":205},"/pricing/","pricing",{"text":207,"config":208,"link":210,"lists":214,"feature":298},"Resources",{"dataNavLevelOne":209},"resources",{"text":211,"config":212},"View all resources",{"href":213,"dataGaName":209,"dataGaLocation":44},"/resources/",[215,248,270],{"title":216,"items":217},"Getting started",[218,223,228,233,238,243],{"text":219,"config":220},"Install",{"href":221,"dataGaName":222,"dataGaLocation":44},"/install/","install",{"text":224,"config":225},"Quick start guides",{"href":226,"dataGaName":227,"dataGaLocation":44},"/get-started/","quick setup checklists",{"text":229,"config":230},"Learn",{"href":231,"dataGaLocation":44,"dataGaName":232},"https://university.gitlab.com/","learn",{"text":234,"config":235},"Product documentation",{"href":236,"dataGaName":237,"dataGaLocation":44},"https://docs.gitlab.com/","product documentation",{"text":239,"config":240},"Best practice videos",{"href":241,"dataGaName":242,"dataGaLocation":44},"/getting-started-videos/","best practice videos",{"text":244,"config":245},"Integrations",{"href":246,"dataGaName":247,"dataGaLocation":44},"/integrations/","integrations",{"title":249,"items":250},"Discover",[251,256,260,265],{"text":252,"config":253},"Customer success stories",{"href":254,"dataGaName":255,"dataGaLocation":44},"/customers/","customer success stories",{"text":257,"config":258},"Blog",{"href":259,"dataGaName":5,"dataGaLocation":44},"/blog/",{"text":261,"config":262},"Remote",{"href":263,"dataGaName":264,"dataGaLocation":44},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":266,"config":267},"TeamOps",{"href":268,"dataGaName":269,"dataGaLocation":44},"/teamops/","teamops",{"title":271,"items":272},"Connect",[273,278,283,288,293],{"text":274,"config":275},"GitLab Services",{"href":276,"dataGaName":277,"dataGaLocation":44},"/services/","services",{"text":279,"config":280},"Community",{"href":281,"dataGaName":282,"dataGaLocation":44},"/community/","community",{"text":284,"config":285},"Forum",{"href":286,"dataGaName":287,"dataGaLocation":44},"https://forum.gitlab.com/","forum",{"text":289,"config":290},"Events",{"href":291,"dataGaName":292,"dataGaLocation":44},"/events/","events",{"text":294,"config":295},"Partners",{"href":296,"dataGaName":297,"dataGaLocation":44},"/partners/","partners",{"backgroundColor":299,"textColor":300,"text":301,"image":302,"link":306},"#2f2a6b","#fff","Insights for the future of software development",{"altText":303,"config":304},"the source promo card",{"src":305},"/images/navigation/the-source-promo-card.svg",{"text":307,"config":308},"Read the latest",{"href":309,"dataGaName":310,"dataGaLocation":44},"/the-source/","the source",{"text":312,"config":313,"lists":315},"Company",{"dataNavLevelOne":314},"company",[316],{"items":317},[318,323,329,331,336,341,346,351,356,361,366],{"text":319,"config":320},"About",{"href":321,"dataGaName":322,"dataGaLocation":44},"/company/","about",{"text":324,"config":325,"footerGa":328},"Jobs",{"href":326,"dataGaName":327,"dataGaLocation":44},"/jobs/","jobs",{"dataGaName":327},{"text":289,"config":330},{"href":291,"dataGaName":292,"dataGaLocation":44},{"text":332,"config":333},"Leadership",{"href":334,"dataGaName":335,"dataGaLocation":44},"/company/team/e-group/","leadership",{"text":337,"config":338},"Team",{"href":339,"dataGaName":340,"dataGaLocation":44},"/company/team/","team",{"text":342,"config":343},"Handbook",{"href":344,"dataGaName":345,"dataGaLocation":44},"https://handbook.gitlab.com/","handbook",{"text":347,"config":348},"Investor relations",{"href":349,"dataGaName":350,"dataGaLocation":44},"https://ir.gitlab.com/","investor relations",{"text":352,"config":353},"Trust Center",{"href":354,"dataGaName":355,"dataGaLocation":44},"/security/","trust center",{"text":357,"config":358},"AI Transparency Center",{"href":359,"dataGaName":360,"dataGaLocation":44},"/ai-transparency-center/","ai transparency center",{"text":362,"config":363},"Newsletter",{"href":364,"dataGaName":365,"dataGaLocation":44},"/company/contact/","newsletter",{"text":367,"config":368},"Press",{"href":369,"dataGaName":370,"dataGaLocation":44},"/press/","press",{"text":372,"config":373,"lists":374},"Contact us",{"dataNavLevelOne":314},[375],{"items":376},[377,380,385],{"text":51,"config":378},{"href":53,"dataGaName":379,"dataGaLocation":44},"talk to sales",{"text":381,"config":382},"Get help",{"href":383,"dataGaName":384,"dataGaLocation":44},"/support/","get help",{"text":386,"config":387},"Customer portal",{"href":388,"dataGaName":389,"dataGaLocation":44},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":391,"login":392,"suggestions":399},"Close",{"text":393,"link":394},"To search repositories and projects, login to",{"text":395,"config":396},"gitlab.com",{"href":58,"dataGaName":397,"dataGaLocation":398},"search login","search",{"text":400,"default":401},"Suggestions",[402,404,408,410,414,418],{"text":73,"config":403},{"href":78,"dataGaName":73,"dataGaLocation":398},{"text":405,"config":406},"Code Suggestions (AI)",{"href":407,"dataGaName":405,"dataGaLocation":398},"/solutions/code-suggestions/",{"text":125,"config":409},{"href":127,"dataGaName":125,"dataGaLocation":398},{"text":411,"config":412},"GitLab on AWS",{"href":413,"dataGaName":411,"dataGaLocation":398},"/partners/technology-partners/aws/",{"text":415,"config":416},"GitLab on Google Cloud",{"href":417,"dataGaName":415,"dataGaLocation":398},"/partners/technology-partners/google-cloud-platform/",{"text":419,"config":420},"Why GitLab?",{"href":86,"dataGaName":419,"dataGaLocation":398},{"freeTrial":422,"mobileIcon":427,"desktopIcon":432,"secondaryButton":435},{"text":423,"config":424},"Start free trial",{"href":425,"dataGaName":49,"dataGaLocation":426},"https://gitlab.com/-/trials/new/","nav",{"altText":428,"config":429},"Gitlab Icon",{"src":430,"dataGaName":431,"dataGaLocation":426},"/images/brand/gitlab-logo-tanuki.svg","gitlab icon",{"altText":428,"config":433},{"src":434,"dataGaName":431,"dataGaLocation":426},"/images/brand/gitlab-logo-type.svg",{"text":436,"config":437},"Get Started",{"href":438,"dataGaName":439,"dataGaLocation":426},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":441,"mobileIcon":445,"desktopIcon":447},{"text":442,"config":443},"Learn more about GitLab Duo",{"href":78,"dataGaName":444,"dataGaLocation":426},"gitlab duo",{"altText":428,"config":446},{"src":430,"dataGaName":431,"dataGaLocation":426},{"altText":428,"config":448},{"src":434,"dataGaName":431,"dataGaLocation":426},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":454,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"title":455,"button":456,"image":460,"config":463,"_id":465,"_type":30,"_source":32,"_file":466,"_stem":467,"_extension":35},"/shared/en-us/banner","is now in public beta!",{"text":84,"config":457},{"href":458,"dataGaName":459,"dataGaLocation":44},"/gitlab-duo/agent-platform/","duo banner",{"config":461},{"src":462},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1753720689/somrf9zaunk0xlt7ne4x.svg",{"layout":464},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":469,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"data":470,"_id":675,"_type":30,"title":676,"_source":32,"_file":677,"_stem":678,"_extension":35},"/shared/en-us/main-footer",{"text":471,"source":472,"edit":478,"contribute":483,"config":488,"items":493,"minimal":667},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":473,"config":474},"View page source",{"href":475,"dataGaName":476,"dataGaLocation":477},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":479,"config":480},"Edit this page",{"href":481,"dataGaName":482,"dataGaLocation":477},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":484,"config":485},"Please contribute",{"href":486,"dataGaName":487,"dataGaLocation":477},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":489,"facebook":490,"youtube":491,"linkedin":492},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[494,517,574,603,637],{"title":62,"links":495,"subMenu":500},[496],{"text":497,"config":498},"DevSecOps platform",{"href":71,"dataGaName":499,"dataGaLocation":477},"devsecops platform",[501],{"title":202,"links":502},[503,507,512],{"text":504,"config":505},"View plans",{"href":204,"dataGaName":506,"dataGaLocation":477},"view plans",{"text":508,"config":509},"Why Premium?",{"href":510,"dataGaName":511,"dataGaLocation":477},"/pricing/premium/","why premium",{"text":513,"config":514},"Why Ultimate?",{"href":515,"dataGaName":516,"dataGaLocation":477},"/pricing/ultimate/","why ultimate",{"title":518,"links":519},"Solutions",[520,525,528,530,535,540,544,547,551,556,558,561,564,569],{"text":521,"config":522},"Digital transformation",{"href":523,"dataGaName":524,"dataGaLocation":477},"/topics/digital-transformation/","digital transformation",{"text":150,"config":526},{"href":145,"dataGaName":527,"dataGaLocation":477},"security & compliance",{"text":139,"config":529},{"href":121,"dataGaName":122,"dataGaLocation":477},{"text":531,"config":532},"Agile development",{"href":533,"dataGaName":534,"dataGaLocation":477},"/solutions/agile-delivery/","agile delivery",{"text":536,"config":537},"Cloud transformation",{"href":538,"dataGaName":539,"dataGaLocation":477},"/topics/cloud-native/","cloud transformation",{"text":541,"config":542},"SCM",{"href":135,"dataGaName":543,"dataGaLocation":477},"source code management",{"text":125,"config":545},{"href":127,"dataGaName":546,"dataGaLocation":477},"continuous integration & delivery",{"text":548,"config":549},"Value stream management",{"href":177,"dataGaName":550,"dataGaLocation":477},"value stream management",{"text":552,"config":553},"GitOps",{"href":554,"dataGaName":555,"dataGaLocation":477},"/solutions/gitops/","gitops",{"text":187,"config":557},{"href":189,"dataGaName":190,"dataGaLocation":477},{"text":559,"config":560},"Small business",{"href":194,"dataGaName":195,"dataGaLocation":477},{"text":562,"config":563},"Public sector",{"href":199,"dataGaName":200,"dataGaLocation":477},{"text":565,"config":566},"Education",{"href":567,"dataGaName":568,"dataGaLocation":477},"/solutions/education/","education",{"text":570,"config":571},"Financial services",{"href":572,"dataGaName":573,"dataGaLocation":477},"/solutions/finance/","financial services",{"title":207,"links":575},[576,578,580,582,585,587,589,591,593,595,597,599,601],{"text":219,"config":577},{"href":221,"dataGaName":222,"dataGaLocation":477},{"text":224,"config":579},{"href":226,"dataGaName":227,"dataGaLocation":477},{"text":229,"config":581},{"href":231,"dataGaName":232,"dataGaLocation":477},{"text":234,"config":583},{"href":236,"dataGaName":584,"dataGaLocation":477},"docs",{"text":257,"config":586},{"href":259,"dataGaName":5,"dataGaLocation":477},{"text":252,"config":588},{"href":254,"dataGaName":255,"dataGaLocation":477},{"text":261,"config":590},{"href":263,"dataGaName":264,"dataGaLocation":477},{"text":274,"config":592},{"href":276,"dataGaName":277,"dataGaLocation":477},{"text":266,"config":594},{"href":268,"dataGaName":269,"dataGaLocation":477},{"text":279,"config":596},{"href":281,"dataGaName":282,"dataGaLocation":477},{"text":284,"config":598},{"href":286,"dataGaName":287,"dataGaLocation":477},{"text":289,"config":600},{"href":291,"dataGaName":292,"dataGaLocation":477},{"text":294,"config":602},{"href":296,"dataGaName":297,"dataGaLocation":477},{"title":312,"links":604},[605,607,609,611,613,615,617,621,626,628,630,632],{"text":319,"config":606},{"href":321,"dataGaName":314,"dataGaLocation":477},{"text":324,"config":608},{"href":326,"dataGaName":327,"dataGaLocation":477},{"text":332,"config":610},{"href":334,"dataGaName":335,"dataGaLocation":477},{"text":337,"config":612},{"href":339,"dataGaName":340,"dataGaLocation":477},{"text":342,"config":614},{"href":344,"dataGaName":345,"dataGaLocation":477},{"text":347,"config":616},{"href":349,"dataGaName":350,"dataGaLocation":477},{"text":618,"config":619},"Sustainability",{"href":620,"dataGaName":618,"dataGaLocation":477},"/sustainability/",{"text":622,"config":623},"Diversity, inclusion and belonging (DIB)",{"href":624,"dataGaName":625,"dataGaLocation":477},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":352,"config":627},{"href":354,"dataGaName":355,"dataGaLocation":477},{"text":362,"config":629},{"href":364,"dataGaName":365,"dataGaLocation":477},{"text":367,"config":631},{"href":369,"dataGaName":370,"dataGaLocation":477},{"text":633,"config":634},"Modern Slavery Transparency Statement",{"href":635,"dataGaName":636,"dataGaLocation":477},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"title":638,"links":639},"Contact Us",[640,643,645,647,652,657,662],{"text":641,"config":642},"Contact an expert",{"href":53,"dataGaName":54,"dataGaLocation":477},{"text":381,"config":644},{"href":383,"dataGaName":384,"dataGaLocation":477},{"text":386,"config":646},{"href":388,"dataGaName":389,"dataGaLocation":477},{"text":648,"config":649},"Status",{"href":650,"dataGaName":651,"dataGaLocation":477},"https://status.gitlab.com/","status",{"text":653,"config":654},"Terms of use",{"href":655,"dataGaName":656,"dataGaLocation":477},"/terms/","terms of use",{"text":658,"config":659},"Privacy statement",{"href":660,"dataGaName":661,"dataGaLocation":477},"/privacy/","privacy statement",{"text":663,"config":664},"Cookie preferences",{"dataGaName":665,"dataGaLocation":477,"id":666,"isOneTrustButton":107},"cookie preferences","ot-sdk-btn",{"items":668},[669,671,673],{"text":653,"config":670},{"href":655,"dataGaName":656,"dataGaLocation":477},{"text":658,"config":672},{"href":660,"dataGaName":661,"dataGaLocation":477},{"text":663,"config":674},{"dataGaName":665,"dataGaLocation":477,"id":666,"isOneTrustButton":107},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[680],{"_path":681,"_dir":682,"_draft":6,"_partial":6,"_locale":7,"content":683,"config":687,"_id":689,"_type":30,"title":690,"_source":32,"_file":691,"_stem":692,"_extension":35},"/en-us/blog/authors/matthias-kppler","authors",{"name":18,"config":684},{"headshot":685,"ctfId":686},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749670351/Blog/Author%20Headshots/mkaeppler-headshot.jpg","mkaeppler",{"template":688},"BlogAuthor","content:en-us:blog:authors:matthias-kppler.yml","Matthias Kppler","en-us/blog/authors/matthias-kppler.yml","en-us/blog/authors/matthias-kppler",{"_path":694,"_dir":38,"_draft":6,"_partial":6,"_locale":7,"header":695,"eyebrow":696,"blurb":697,"button":698,"secondaryButton":702,"_id":704,"_type":30,"title":705,"_source":32,"_file":706,"_stem":707,"_extension":35},"/shared/en-us/next-steps","Start shipping better software faster","50%+ of the Fortune 100 trust GitLab","See what your team can do with the intelligent\n\n\nDevSecOps platform.\n",{"text":46,"config":699},{"href":700,"dataGaName":49,"dataGaLocation":701},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":51,"config":703},{"href":53,"dataGaName":54,"dataGaLocation":701},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",1753981647855]