A GitLab CI/CD pipeline can either feel like a productivity multiplier or a daily source of friction. Fast, readable, reliable pipelines give teams confidence to ship. Slow, repetitive, fragile pipelines create long feedback loops, hidden waste, and risky release habits. That is why GitLab CI best practices matter. Good pipeline design is not just about valid YAML. It is about reducing developer wait time, making failures obvious, protecting sensitive operations, and keeping CI logic maintainable as the codebase grows.
This tutorial covers practical GitLab pipeline optimization principles you can apply whether you manage one application or dozens. The goal is not to make the most complex pipeline possible. The goal is to build efficient pipelines that are fast, clear, secure, and scalable.
Speed is the first pipeline feature users notice. If basic feedback takes too long, developers wait, context-switch, or skip checks locally. Fast pipelines improve both delivery flow and code quality.
Ways to keep GitLab CI pipelines fast include:
needs to start downstream jobs earlyFor example, this pipeline is serialized more than necessary:
stages:
- build
- test
- deployIf build, lint, and unit tests do not all depend on each other, you can often redesign the graph so jobs start sooner.
Also pay attention to Docker images. Pulling large images repeatedly is expensive. If a job only needs Node.js, use a focused Node image instead of a general-purpose image packed with tools you do not need.
Not all failures are equal. If linting or unit tests fail in 45 seconds, there is no reason to wait 15 minutes for a slow deployment package step to tell you the pipeline failed.
Put the fastest and most failure-prone feedback early:
Then run heavier jobs such as long integration suites, container builds, and deployment jobs after the basics pass.
This is both a technical and human optimization. The earlier the pipeline tells a developer something is wrong, the less time is wasted.
A strong fail-fast pattern is:
stages:
- validate
- test
- package
- deployEven better, combine this with DAG-style needs so jobs do not wait on unrelated stage barriers.
Caching is one of the most important GitLab pipeline optimization tools. A good cache prevents repeated downloading and setup work across jobs and pipeline runs.
cache:
key:
files:
- package-lock.json
paths:
- .npm/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"cache:
key:
files:
- requirements.txt
paths:
- .pip-cache/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"cache:
key: maven-$CI_COMMIT_REF_SLUG
paths:
- .m2/repository/The main best practice is to cache dependency directories or package manager caches, not random build outputs. Use cache for things that improve performance and can be safely recreated.
Also remember that cache keys matter. A lock-file based key is much better than one permanent key that never changes because it refreshes naturally when dependencies change.
Artifacts are essential when one job must pass real outputs to another job, but they are often overused. Huge artifact archives increase storage usage and slow job downloads.
Good artifact practice means:
Bad pattern:
artifacts:
paths:
- .Good pattern:
artifacts:
paths:
- dist/
expire_in: 1 weekIf the next job only needs dist/, do not archive the entire workspace.
Repeated CI configuration becomes hard to maintain. If five jobs all install the same dependencies, use YAML anchors and aliases to define the shared behavior once.
Example:
.default_node_job: &default_node_job
image: node:20
before_script:
- npm ci
lint:
<<: *default_node_job
stage: validate
script:
- npm run lint
test:
<<: *default_node_job
stage: test
script:
- npm testThis keeps the file smaller and reduces copy-paste errors.
Anchors are especially useful for:
before_scriptinclude to Share Pipeline TemplatesYAML anchors help inside one file. GitLab include helps across files and projects.
Use include when:
.gitlab-ci.yml is getting too largeExample:
include:
- project: my-group/pipeline-library
ref: main
file: /templates/node-service.ymlThis is one of the best GitLab CI templates patterns because it centralizes maintenance. When shared logic improves, multiple projects benefit.
The trade-off is governance: shared templates should be versioned carefully so a breaking change in the template does not surprise many repositories at once.
One of the most basic GitLab CI best practices is also one of the most important: never hardcode secrets in .gitlab-ci.yml.
Bad example:
variables:
AWS_SECRET_ACCESS_KEY: abc123supersecretCorrect approach:
This matters because .gitlab-ci.yml is source code. If you put a secret there, it enters Git history and becomes much harder to contain.
needs for DAG PipelinesTraditional stage ordering forces all jobs in one stage to finish before the next stage begins. That is simple, but often slower than necessary.
The needs keyword lets you build a DAG pipeline, where jobs start as soon as their specific dependencies are ready.
Example:
stages:
- validate
- test
- package
lint:
stage: validate
script: npm run lint
unit_tests:
stage: test
script: npm test
build_image:
stage: package
needs:
- unit_tests
script: docker build -t myapp .With needs, build_image does not wait for every job in previous stages. It waits only for the job it actually depends on.
This is one of the biggest improvements you can make to pipeline speed in GitLab.
rules Instead of Only only/exceptOlder pipelines often rely on only and except. They still exist, but rules is generally more expressive and easier to understand in modern GitLab CI/CD.
With rules, you can control job creation based on:
Example:
deploy_production:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- when: never
script:
- ./deploy.sh productionThis is clearer than trying to combine only, except, and other conditions across multiple jobs.
Not every deployment should be automatic. For sensitive environments, introduce deliberate control.
Good production protection patterns include:
when: manual for release approval stepsExample:
deploy_production:
stage: deploy
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
script:
- ./deploy.sh productionThis does not mean every environment needs manual approval. It means the highest-risk environments should not be too easy to change by accident.
Even experienced teams fall into predictable CI pitfalls.
If every job re-downloads dependencies from scratch, the pipeline will stay slow.
latest images without pinninglatest looks convenient, but it hurts reproducibility. Pin images to known versions:
image: node:20.16.0Some pipelines create a new stage for every conceptual step. That often slows everything down because stage barriers multiply. Prefer fewer stages with clearer DAG dependencies.
Saving the whole workspace from every job wastes time and storage.
If every repo has nearly identical pipeline logic, invest in templates and includes.
Even if secrets are stored correctly, careless commands can print them. Review job scripts for accidental echoing of sensitive values.
Optimization works best when you measure before and after. GitLab CI provides analytics and job timing data that help you answer questions such as:
Use pipeline analytics, job duration views, and merge request timing trends to guide improvements. Do not optimize blindly. Focus first on the jobs that consume the most time or fail most often.
For example, if a pipeline takes 18 minutes and 10 of those minutes come from dependency installation, caching may have a larger impact than any YAML refactor. If the problem is a serialized deployment graph, needs may be the best fix.
Here is a deliberately inefficient pipeline.
stages:
- install
- lint
- test
- build
- package
- deploy
install:
stage: install
image: node:latest
script:
- npm ci
lint:
stage: lint
image: node:latest
script:
- npm ci
- npm run lint
test:
stage: test
image: node:latest
script:
- npm ci
- npm test
build:
stage: build
image: node:latest
script:
- npm ci
- npm run build
artifacts:
paths:
- .
package:
stage: package
script:
- docker build -t myapp .
deploy:
stage: deploy
script:
- ./deploy.shProblems:
node:latest is unpinnednpm ci repeats in every job without cachingstages:
- validate
- test
- package
- deploy
.default_node_job: &default_node_job
image: node:20.16.0
cache:
key:
files:
- package-lock.json
paths:
- .npm/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
before_script:
- npm ci
lint:
<<: *default_node_job
stage: validate
script:
- npm run lint
unit_tests:
<<: *default_node_job
stage: test
script:
- npm test
build_app:
<<: *default_node_job
stage: package
needs:
- unit_tests
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
build_image:
stage: package
image: docker:27
needs:
- build_app
script:
- docker build -t myapp:$CI_COMMIT_SHA .
deploy_production:
stage: deploy
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- when: never
script:
- ./deploy.sh productionWhy the refactor is better:
needs for faster package stepsThat is what GitLab CI best practices look like in action: not flashy YAML tricks, but clearer and faster automation.
The best GitLab pipelines are fast, focused, and maintainable. Keep feedback loops short, cache expensive dependency work, use artifacts deliberately, reduce duplication with anchors and includes, prefer rules, accelerate flow with needs, and protect production with manual gates and protected environments. If you apply those principles consistently, your GitLab CI/CD setup becomes easier to trust and easier to scale.
Pipeline optimization is rarely about one magic keyword. It is about many small improvements that remove waste from the delivery path.
<MCQExercise exercise={{ id: 'gitlab-best-practices-q1', title: 'Question 1: Pipeline Speed', description: "Which GitLab CI feature is especially useful for starting jobs as soon as their real dependencies are ready instead of waiting for an entire previous stage to finish?", options: [ { id: 'a', text: 'needs' }, { id: 'b', text: 'expire_in' }, { id: 'c', text: 'when: delayed' }, { id: 'd', text: 'trigger' }, ], correctId: 'a', explanation: "The needs keyword creates DAG-style dependencies, which lets jobs begin earlier and often shortens total pipeline duration.", wrongExplanations: { b: 'expire_in controls artifact retention, not dependency execution order.', c: 'when: delayed postpones a job instead of speeding the pipeline up.', d: 'trigger starts downstream pipelines, but it does not replace DAG dependencies inside a pipeline.', }, }} />
<MCQExercise exercise={{ id: 'gitlab-best-practices-q2', title: 'Question 2: Secrets Management', description: "What is the best practice for handling secrets in GitLab CI/CD?", options: [ { id: 'a', text: 'Store them directly in .gitlab-ci.yml so every job can read them easily' }, { id: 'b', text: 'Echo them during the job so debugging is easier' }, { id: 'c', text: 'Store them as masked and protected CI/CD variables or use federation such as OIDC' }, { id: 'd', text: 'Commit them in a private branch only' }, ], correctId: 'c', explanation: "Secrets should live outside the pipeline definition in GitLab CI/CD variables or external identity systems, with masking and protection applied where appropriate.", wrongExplanations: { a: 'Hardcoding secrets in the pipeline definition puts them into version control history.', b: 'Printing secrets in logs creates exposure risk and defeats masking practices.', d: 'A private branch is still source control and is not a safe place for secrets.', }, }} />
<MCQExercise exercise={{ id: 'gitlab-best-practices-q3', title: 'Question 3: DRY Pipelines', description: "Why would you use YAML anchors and the include keyword in GitLab CI/CD?", options: [ { id: 'a', text: 'To make pipelines slower but more detailed' }, { id: 'b', text: 'To reduce duplication and share reusable pipeline logic within and across projects' }, { id: 'c', text: 'To disable caching automatically' }, { id: 'd', text: 'To force every deployment to run on every branch' }, ], correctId: 'b', explanation: "YAML anchors help reuse patterns inside a file, and include helps share pipeline templates across files or repositories, which makes CI configuration easier to maintain.", wrongExplanations: { a: 'Their purpose is maintainability and reuse, not intentionally making pipelines slower.', c: 'Anchors and include do not disable caching; they can actually help standardize cache configuration.', d: 'Branch behavior is controlled by rules and workflow logic, not by anchors or include alone.', }, }} />
Anchors are a YAML feature that copies a block wherever it is referenced, and they only work within a single file. extends: is a GitLab keyword that merges a job template into another job, works across included files, and merges nested keys intelligently rather than replacing them wholesale. extends: is the better choice for anything shared, particularly once configuration is split with include:, because anchors cannot cross file boundaries.
Split it. Keep the root file as a short index of include: statements plus the stages: and workflow: definitions, and move job definitions into files grouped by concern. Build, test, deploy, security. Define shared job templates once and specialise them with extends:. For templates used by several projects, move them into a dedicated repository and include them from there with a pinned ref so a change to the shared file cannot break every consumer at once.
Hardcoding secrets in .gitlab-ci.yml instead of using masked and protected variables; depending on cache for correctness rather than only for speed; keeping every artifact indefinitely until the storage quota is hit; using only/except in new pipelines rather than rules; leaving every job in a strict stage sequence when needs: would let independent work run concurrently; and deploying moving tags such as latest so a rollback has no specific artifact to return to.
No, and a pipeline that does is usually slower than it needs to be. Use rules so expensive jobs run where they add value. A full integration suite on merge requests to the default branch and on a schedule, rather than on every push to every feature branch. rules:changes skips jobs when the relevant files were not touched, which matters enormously in a monorepo. The balance to aim for is that every push gets fast, meaningful feedback, and the exhaustive checks run before merge.
Three mechanisms together. Mark the production credentials as protected variables so only protected branches and tags can read them. Use a protected environment, which lets GitLab enforce who may deploy and can require approvals. And set when: manual on the deploy job so a person deliberately triggers it. Each covers a different gap. The variable stops an untrusted branch reading secrets, the environment stops an unauthorised person deploying, and the manual gate stops an accidental deploy.
2d:["$","$L2e",null,{"tutorial":{"slug":"introduction-to-cicd","title":"Introduction to CI/CD","seoTitle":"What Is CI/CD? A Beginner's Guide to Pipelines","description":"Learn what CI/CD means: how continuous integration, delivery and deployment differ, why teams need automation and what a modern pipeline looks like.","content":"$2f","difficulty":"Easy","duration":"12 min read","keywords":["what is cicd","ci cd explained","continuous integration continuous delivery","continuous deployment vs continuous delivery","cicd pipeline explained","what is a cicd pipeline","cicd for beginners","devops cicd","gitlab cicd introduction","software deployment automation"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":1,"tableOfContents":[{"id":"the-problem-before-cicd","title":"The Problem Before CI/CD","level":2},{"id":"what-integration-hell-looked-like","title":"What integration hell looked like","level":3},{"id":"manual-deployments-made-everything-worse","title":"Manual deployments made everything worse","level":3},{"id":"late-stage-bugs-were-expensive","title":"Late-stage bugs were expensive","level":3},{"id":"what-is-continuous-integration-ci","title":"What Is Continuous Integration (CI)?","level":2},{"id":"what-happens-in-continuous-integration","title":"What happens in Continuous Integration","level":3},{"id":"why-frequent-merging-matters","title":"Why frequent merging matters","level":3},{"id":"the-real-goal-of-ci","title":"The real goal of CI","level":3},{"id":"what-is-continuous-delivery-cd","title":"What Is Continuous Delivery (CD)?","level":2},{"id":"the-push-button-release-idea","title":"The push-button release idea","level":3},{"id":"why-teams-like-continuous-delivery","title":"Why teams like Continuous Delivery","level":3},{"id":"what-is-continuous-deployment","title":"What Is Continuous Deployment?","level":2},{"id":"delivery-vs-deployment","title":"Delivery vs Deployment","level":3},{"id":"when-continuous-deployment-works-well","title":"When Continuous Deployment works well","level":3},{"id":"ci-vs-cd-vs-continuous-deployment","title":"CI vs CD vs Continuous Deployment","level":2},{"id":"the-anatomy-of-a-cicd-pipeline","title":"The Anatomy of a CI/CD Pipeline","level":2},{"id":"stages","title":"Stages","level":3},{"id":"jobs","title":"Jobs","level":3},{"id":"triggers","title":"Triggers","level":3},{"id":"key-cicd-concepts-you-should-know","title":"Key CI/CD Concepts You Should Know","level":2},{"id":"pipeline","title":"Pipeline","level":3},{"id":"job","title":"Job","level":3},{"id":"stage","title":"Stage","level":3},{"id":"artifact","title":"Artifact","level":3},{"id":"runner","title":"Runner","level":3},{"id":"trigger","title":"Trigger","level":3},{"id":"a-simple-mental-model-commit---pipeline---feedback-loop","title":"A Simple Mental Model: Commit -> Pipeline -> Feedback Loop","level":2},{"id":"benefits-of-cicd","title":"Benefits of CI/CD","level":2},{"id":"faster-release-cycles","title":"Faster release cycles","level":3},{"id":"fewer-bugs-in-production","title":"Fewer bugs in production","level":3},{"id":"higher-developer-confidence","title":"Higher developer confidence","level":3},{"id":"better-consistency","title":"Better consistency","level":3},{"id":"clear-audit-trail","title":"Clear audit trail","level":3},{"id":"easier-collaboration","title":"Easier collaboration","level":3},{"id":"a-real-pipeline-narrative-without-code","title":"A Real Pipeline Narrative Without Code","level":2},{"id":"the-cicd-tools-landscape","title":"The CI/CD Tools Landscape","level":2},{"id":"gitlab-cicd","title":"GitLab CI/CD","level":3},{"id":"github-actions","title":"GitHub Actions","level":3},{"id":"jenkins","title":"Jenkins","level":3},{"id":"circleci","title":"CircleCI","level":3},{"id":"argo-cd","title":"Argo CD","level":3},{"id":"quick-comparison","title":"Quick comparison","level":3},{"id":"why-gitlab-cicd-is-a-great-choice","title":"Why GitLab CI/CD Is a Great Choice","level":2},{"id":"built-in-not-bolted-on","title":"Built in, not bolted on","level":3},{"id":"pipeline-as-code","title":"Pipeline as code","level":3},{"id":"no-separate-server-required-for-basic-use","title":"No separate server required for basic use","level":3},{"id":"strong-visibility","title":"Strong visibility","level":3},{"id":"scales-with-maturity","title":"Scales with maturity","level":3},{"id":"common-misunderstandings-beginners-have","title":"Common Misunderstandings Beginners Have","level":2},{"id":"cicd-is-only-for-big-companies","title":"âCI/CD is only for big companiesâ","level":3},{"id":"cicd-means-automatic-production-deploys","title":"âCI/CD means automatic production deploysâ","level":3},{"id":"a-pipeline-is-just-testing","title":"âA pipeline is just testingâ","level":3},{"id":"we-can-add-cicd-later","title":"âWe can add CI/CD laterâ","level":3},{"id":"bringing-it-all-together","title":"Bringing It All Together","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between continuous integration, continuous delivery and continuous deployment?","answer":"Continuous integration is about merging work frequently and verifying each change automatically. Every commit triggers a build and a test run, so integration problems surface within minutes instead of at the end of a sprint. Continuous delivery extends that so every change that passes is packaged and provably releasable, with the final push to production left as a human decision. Continuous deployment removes that last manual gate: anything passing the pipeline goes live automatically. The abbreviation CD is genuinely ambiguous and covers the second and third, which is why it is worth being explicit about which one a team means."},{"question":"Do I need CI/CD for a small project?","answer":"The continuous integration half pays for itself almost immediately, even solo. A pipeline that runs your tests on every push takes an hour to set up and catches the class of mistake that is otherwise found by a user. The deployment half is more of a judgement call, if you deploy once a month by hand and it works, automating it can wait. A reasonable order is tests on every commit first, then automated builds, then automated deployment to staging, and production deployment last once you trust the earlier stages."},{"question":"How long should a CI pipeline take?","answer":"Under ten minutes for the feedback developers wait on, and ideally under five. The number matters because it changes behaviour: past roughly fifteen minutes people stop waiting for results, start batching changes, and the fast feedback that justifies CI disappears. When a pipeline grows too slow, the usual wins are running independent jobs in parallel, caching dependencies between runs, and splitting long test suites so only the affected parts run on every commit while the full suite runs on merge."},{"question":"What is the difference between a pipeline, a stage and a job?","answer":"A job is a single unit of work, run the tests, build the image, deploy to staging, executed by a runner in its own environment. A stage is a named group of jobs that run in parallel with each other, such as build, test and deploy. A pipeline is the whole sequence of stages triggered by an event, usually a push or a merge request. Stages run in order and, by default, a stage only begins once every job in the previous one has succeeded."},{"question":"Which CI/CD tool should I learn?","answer":"The concepts transfer almost completely, so the specific tool matters less than people expect. Stages, jobs, runners, artifacts and caching exist in all of them under slightly different names. GitLab CI/CD is a good one to learn first because the whole pipeline is defined in a single .gitlab-ci.yml file in your repository and the platform includes the registry, runners and environments without extra setup. GitHub Actions is the other obvious choice given how much code lives on GitHub. Learning either one well makes picking up the other a matter of hours."},{"question":"Does CI/CD mean deploying to production automatically?","answer":"No, and conflating the two puts a lot of teams off. Continuous integration is only about verifying changes automatically, and it delivers most of the value on its own. Automated deployment to production is a separate decision that depends on your test coverage, your ability to roll back quickly, and your appetite for risk. Plenty of mature teams run excellent CI with a deliberate human approval before production, and that is a perfectly legitimate place to stop."}]},"allTutorials":[{"slug":"introduction-to-cicd","title":"Introduction to CI/CD","seoTitle":"What Is CI/CD? A Beginner's Guide to Pipelines","description":"Learn what CI/CD means: how continuous integration, delivery and deployment differ, why teams need automation and what a modern pipeline looks like.","content":"$30","difficulty":"Easy","duration":"12 min read","keywords":"$2d:props:tutorial:keywords","publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":1,"tableOfContents":[{"id":"the-problem-before-cicd","title":"The Problem Before CI/CD","level":2},{"id":"what-integration-hell-looked-like","title":"What integration hell looked like","level":3},{"id":"manual-deployments-made-everything-worse","title":"Manual deployments made everything worse","level":3},{"id":"late-stage-bugs-were-expensive","title":"Late-stage bugs were expensive","level":3},{"id":"what-is-continuous-integration-ci","title":"What Is Continuous Integration (CI)?","level":2},{"id":"what-happens-in-continuous-integration","title":"What happens in Continuous Integration","level":3},{"id":"why-frequent-merging-matters","title":"Why frequent merging matters","level":3},{"id":"the-real-goal-of-ci","title":"The real goal of CI","level":3},{"id":"what-is-continuous-delivery-cd","title":"What Is Continuous Delivery (CD)?","level":2},{"id":"the-push-button-release-idea","title":"The push-button release idea","level":3},{"id":"why-teams-like-continuous-delivery","title":"Why teams like Continuous Delivery","level":3},{"id":"what-is-continuous-deployment","title":"What Is Continuous Deployment?","level":2},{"id":"delivery-vs-deployment","title":"Delivery vs Deployment","level":3},{"id":"when-continuous-deployment-works-well","title":"When Continuous Deployment works well","level":3},{"id":"ci-vs-cd-vs-continuous-deployment","title":"CI vs CD vs Continuous Deployment","level":2},{"id":"the-anatomy-of-a-cicd-pipeline","title":"The Anatomy of a CI/CD Pipeline","level":2},{"id":"stages","title":"Stages","level":3},{"id":"jobs","title":"Jobs","level":3},{"id":"triggers","title":"Triggers","level":3},{"id":"key-cicd-concepts-you-should-know","title":"Key CI/CD Concepts You Should Know","level":2},{"id":"pipeline","title":"Pipeline","level":3},{"id":"job","title":"Job","level":3},{"id":"stage","title":"Stage","level":3},{"id":"artifact","title":"Artifact","level":3},{"id":"runner","title":"Runner","level":3},{"id":"trigger","title":"Trigger","level":3},{"id":"a-simple-mental-model-commit---pipeline---feedback-loop","title":"A Simple Mental Model: Commit -> Pipeline -> Feedback Loop","level":2},{"id":"benefits-of-cicd","title":"Benefits of CI/CD","level":2},{"id":"faster-release-cycles","title":"Faster release cycles","level":3},{"id":"fewer-bugs-in-production","title":"Fewer bugs in production","level":3},{"id":"higher-developer-confidence","title":"Higher developer confidence","level":3},{"id":"better-consistency","title":"Better consistency","level":3},{"id":"clear-audit-trail","title":"Clear audit trail","level":3},{"id":"easier-collaboration","title":"Easier collaboration","level":3},{"id":"a-real-pipeline-narrative-without-code","title":"A Real Pipeline Narrative Without Code","level":2},{"id":"the-cicd-tools-landscape","title":"The CI/CD Tools Landscape","level":2},{"id":"gitlab-cicd","title":"GitLab CI/CD","level":3},{"id":"github-actions","title":"GitHub Actions","level":3},{"id":"jenkins","title":"Jenkins","level":3},{"id":"circleci","title":"CircleCI","level":3},{"id":"argo-cd","title":"Argo CD","level":3},{"id":"quick-comparison","title":"Quick comparison","level":3},{"id":"why-gitlab-cicd-is-a-great-choice","title":"Why GitLab CI/CD Is a Great Choice","level":2},{"id":"built-in-not-bolted-on","title":"Built in, not bolted on","level":3},{"id":"pipeline-as-code","title":"Pipeline as code","level":3},{"id":"no-separate-server-required-for-basic-use","title":"No separate server required for basic use","level":3},{"id":"strong-visibility","title":"Strong visibility","level":3},{"id":"scales-with-maturity","title":"Scales with maturity","level":3},{"id":"common-misunderstandings-beginners-have","title":"Common Misunderstandings Beginners Have","level":2},{"id":"cicd-is-only-for-big-companies","title":"âCI/CD is only for big companiesâ","level":3},{"id":"cicd-means-automatic-production-deploys","title":"âCI/CD means automatic production deploysâ","level":3},{"id":"a-pipeline-is-just-testing","title":"âA pipeline is just testingâ","level":3},{"id":"we-can-add-cicd-later","title":"âWe can add CI/CD laterâ","level":3},{"id":"bringing-it-all-together","title":"Bringing It All Together","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between continuous integration, continuous delivery and continuous deployment?","answer":"Continuous integration is about merging work frequently and verifying each change automatically. Every commit triggers a build and a test run, so integration problems surface within minutes instead of at the end of a sprint. Continuous delivery extends that so every change that passes is packaged and provably releasable, with the final push to production left as a human decision. Continuous deployment removes that last manual gate: anything passing the pipeline goes live automatically. The abbreviation CD is genuinely ambiguous and covers the second and third, which is why it is worth being explicit about which one a team means."},{"question":"Do I need CI/CD for a small project?","answer":"The continuous integration half pays for itself almost immediately, even solo. A pipeline that runs your tests on every push takes an hour to set up and catches the class of mistake that is otherwise found by a user. The deployment half is more of a judgement call, if you deploy once a month by hand and it works, automating it can wait. A reasonable order is tests on every commit first, then automated builds, then automated deployment to staging, and production deployment last once you trust the earlier stages."},{"question":"How long should a CI pipeline take?","answer":"Under ten minutes for the feedback developers wait on, and ideally under five. The number matters because it changes behaviour: past roughly fifteen minutes people stop waiting for results, start batching changes, and the fast feedback that justifies CI disappears. When a pipeline grows too slow, the usual wins are running independent jobs in parallel, caching dependencies between runs, and splitting long test suites so only the affected parts run on every commit while the full suite runs on merge."},{"question":"What is the difference between a pipeline, a stage and a job?","answer":"A job is a single unit of work, run the tests, build the image, deploy to staging, executed by a runner in its own environment. A stage is a named group of jobs that run in parallel with each other, such as build, test and deploy. A pipeline is the whole sequence of stages triggered by an event, usually a push or a merge request. Stages run in order and, by default, a stage only begins once every job in the previous one has succeeded."},{"question":"Which CI/CD tool should I learn?","answer":"The concepts transfer almost completely, so the specific tool matters less than people expect. Stages, jobs, runners, artifacts and caching exist in all of them under slightly different names. GitLab CI/CD is a good one to learn first because the whole pipeline is defined in a single .gitlab-ci.yml file in your repository and the platform includes the registry, runners and environments without extra setup. GitHub Actions is the other obvious choice given how much code lives on GitHub. Learning either one well makes picking up the other a matter of hours."},{"question":"Does CI/CD mean deploying to production automatically?","answer":"No, and conflating the two puts a lot of teams off. Continuous integration is only about verifying changes automatically, and it delivers most of the value on its own. Automated deployment to production is a separate decision that depends on your test coverage, your ability to roll back quickly, and your appetite for risk. Plenty of mature teams run excellent CI with a deliberate human approval before production, and that is a perfectly legitimate place to stop."}]},{"slug":"gitlab-cicd-getting-started","title":"Getting Started with GitLab CI/CD","seoTitle":"GitLab CI/CD Tutorial: Your First .gitlab-ci.yml Pipeline","description":"Create your first GitLab CI/CD pipeline: write a minimal three-stage .gitlab-ci.yml, understand GitLab Runners, trigger a pipeline and read the job output.","content":"$31","difficulty":"Easy","duration":"14 min read","keywords":["gitlab ci cd getting started","first gitlab pipeline","gitlab-ci.yml tutorial","create gitlab pipeline","gitlab ci yml example","gitlab runner tutorial","gitlab ci beginner","how to use gitlab ci","gitlab pipeline first steps","gitlab ci stages example"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":2,"tableOfContents":[{"id":"prerequisites","title":"Prerequisites","level":2},{"id":"1-a-gitlab-account","title":"1. A GitLab account","level":3},{"id":"2-a-repository","title":"2. A repository","level":3},{"id":"3-permission-to-run-pipelines","title":"3. Permission to run pipelines","level":3},{"id":"4-basic-git-workflow-knowledge","title":"4. Basic Git workflow knowledge","level":3},{"id":"what-is-gitlab-ciyml","title":"What Is .gitlab-ci.yml?","level":2},{"id":"where-the-file-goes","title":"Where the file goes","level":3},{"id":"why-the-file-matters","title":"Why the file matters","level":3},{"id":"your-first-gitlab-ciyml-a-minimal-3-stage-pipeline","title":"Your First .gitlab-ci.yml: A Minimal 3-Stage Pipeline","level":2},{"id":"what-this-first-pipeline-teaches-you","title":"What this first pipeline teaches you","level":3},{"id":"breaking-down-each-part-of-the-yaml","title":"Breaking Down Each Part of the YAML","level":2},{"id":"stages","title":"stages","level":3},{"id":"job-names","title":"Job names","level":3},{"id":"stage","title":"stage","level":3},{"id":"script","title":"script","level":3},{"id":"image","title":"image","level":3},{"id":"why-gitlab-uses-runners","title":"Why GitLab Uses Runners","level":2},{"id":"what-a-runner-actually-does","title":"What a runner actually does","level":3},{"id":"shared-runners-vs-self-hosted-runners","title":"Shared Runners vs Self-Hosted Runners","level":2},{"id":"shared-runners","title":"Shared runners","level":3},{"id":"self-hosted-runners","title":"Self-hosted runners","level":3},{"id":"how-gitlab-matches-a-job-to-a-runner","title":"How GitLab Matches a Job to a Runner","level":2},{"id":"shared-pool-matching","title":"Shared pool matching","level":3},{"id":"tagged-runner-matching","title":"Tagged runner matching","level":3},{"id":"why-tags-matter","title":"Why tags matter","level":3},{"id":"triggering-a-pipeline","title":"Triggering a Pipeline","level":2},{"id":"on-push","title":"On push","level":3},{"id":"on-merge-request","title":"On merge request","level":3},{"id":"manually","title":"Manually","level":3},{"id":"on-schedule","title":"On schedule","level":3},{"id":"why-multiple-triggers-are-helpful","title":"Why multiple triggers are helpful","level":3},{"id":"pushing-your-first-pipeline","title":"Pushing Your First Pipeline","level":2},{"id":"watching-the-pipeline-run-in-the-gitlab-ui","title":"Watching the Pipeline Run in the GitLab UI","level":2},{"id":"pipeline-list-view","title":"Pipeline list view","level":3},{"id":"pipeline-graph-view","title":"Pipeline graph view","level":3},{"id":"job-details-and-logs","title":"Job details and logs","level":3},{"id":"green-red-and-other-statuses","title":"Green, red, and other statuses","level":3},{"id":"reading-job-logs","title":"Reading Job Logs","level":2},{"id":"what-the-output-usually-includes","title":"What the output usually includes","level":3},{"id":"how-to-think-about-a-failing-log","title":"How to think about a failing log","level":3},{"id":"common-first-time-mistakes","title":"Common First-Time Mistakes","level":2},{"id":"wrong-yaml-indentation","title":"Wrong YAML indentation","level":3},{"id":"wrong-stage-name","title":"Wrong stage name","level":3},{"id":"forgetting-to-push-gitlab-ciyml","title":"Forgetting to push .gitlab-ci.yml","level":3},{"id":"using-the-wrong-image","title":"Using the wrong image","level":3},{"id":"assuming-every-project-has-a-runner","title":"Assuming every project has a runner","level":3},{"id":"editing-the-file-in-the-wrong-location","title":"Editing the file in the wrong location","level":3},{"id":"building-up-to-a-real-pipeline-add-a-nodejs-test-job","title":"Building Up to a Real Pipeline: Add a Node.js Test Job","level":2},{"id":"why-npm-ci-is-often-preferred","title":"Why npm ci is often preferred","level":3},{"id":"why-the-deploy-step-is-still-a-placeholder","title":"Why the deploy step is still a placeholder","level":3},{"id":"a-complete-working-gitlab-ciyml-example-with-annotations","title":"A Complete Working .gitlab-ci.yml Example with Annotations","level":2},{"id":"how-to-read-this-file","title":"How to read this file","level":3},{"id":"the-pipeline-status-badge-in-your-readme","title":"The Pipeline Status Badge in Your README","level":2},{"id":"debugging-a-failing-job-step-by-step","title":"Debugging a Failing Job Step by Step","level":2},{"id":"step-1-confirm-the-failing-job","title":"Step 1: Confirm the failing job","level":3},{"id":"step-2-open-the-job-log","title":"Step 2: Open the job log","level":3},{"id":"step-3-check-the-runtime-image","title":"Step 3: Check the runtime image","level":3},{"id":"step-4-compare-the-command-locally","title":"Step 4: Compare the command locally","level":3},{"id":"step-5-fix-one-thing-at-a-time","title":"Step 5: Fix one thing at a time","level":3},{"id":"step-6-push-again-and-observe","title":"Step 6: Push again and observe","level":3},{"id":"best-beginner-mindset-for-gitlab-cicd","title":"Best Beginner Mindset for GitLab CI/CD","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Where does the .gitlab-ci.yml file go?","answer":"In the root of your repository, on the branch you want it to run for. GitLab detects it automatically on push and starts a pipeline. There is nothing to enable in the UI for a basic setup. The filename and location matter exactly: a file named gitlab-ci.yml without the leading dot, or one placed in a subdirectory, is ignored silently and you will see no pipeline at all rather than an error. The path can be changed in project settings, but the default is what most examples assume."},{"question":"Why is my pipeline stuck pending?","answer":"No runner has picked the job up. The three common causes are that shared runners are disabled for the project, that your job specifies a tags: value no available runner has, and that you are on a self-managed instance with no registered runner at all. Open the job and GitLab usually states which of these it is. Tag mismatches are the most frequent. A job tagged docker only runs on a runner registered with that same tag, and a typo leaves it waiting indefinitely."},{"question":"What is the difference between shared and self-hosted runners?","answer":"Shared runners are provided by GitLab, require no setup, and are billed by the minute on GitLab.com beyond the free allowance. Self-hosted runners are machines you register and operate yourself, which gives you control over the hardware, lets jobs reach private networks, and removes per-minute costs at the price of maintaining them. Most teams start on shared runners and move specific jobs to self-hosted ones when they need more resources, faster builds through warm caches, or access to internal systems."},{"question":"Why does my job fail with "command not found"?","answer":"The job's Docker image does not contain the tool you are calling. Each job runs in a fresh container from the image: you specify, with nothing inherited from other jobs or from your laptop. If a job runs npm ci it needs a Node image; if it runs aws s3 cp it needs the AWS CLI installed. Either choose an image that already includes the tool, or install it in a before_script. Installing tools on every run is slower, so a purpose-built image is usually worth it once a pipeline stabilises."},{"question":"Do I need a Dockerfile to use GitLab CI/CD?","answer":"No. Jobs run inside a container chosen with the image: keyword, and that image can be any public one, node:22, python:3.12, golang:1.23. You only need your own Dockerfile if you are building an image as part of the pipeline, or if you want a custom job environment with your specific toolchain preinstalled. Beginning with an off-the-shelf image is the normal starting point."},{"question":"How do I see why a job failed?","answer":"Open the job in the GitLab UI and read the log from the bottom up. The failing command and its output are almost always in the last twenty lines, above the exit code. Each job log shows the full script as it ran, so you can see exactly which command produced the error. For problems you cannot reproduce from the log, adding set -x at the start of the script prints each command before running it, which makes variable expansion and path problems visible immediately."}]},{"slug":"gitlab-ci-stages-and-jobs","title":"GitLab CI/CD Pipeline Structure: How Pipelines, Stages, Jobs, Artifacts and Cache Fit Together","seoTitle":"GitLab CI/CD Pipeline Structure: Stages, Jobs, Artifacts","description":"See how a GitLab pipeline is built: how pipelines, stages and jobs relate, what runners do, how needs breaks stage lockstep and where artifacts fit.","content":"$32","difficulty":"Easy","duration":"10 min read","keywords":["gitlab ci pipeline structure","how gitlab pipelines work","gitlab ci architecture","gitlab ci artifacts vs cache","gitlab pipeline types","workflow rules gitlab","gitlab cicd pipeline explained","gitlab ci yml structure","gitlab ci overview"],"publishedDate":"2026-07-12","updatedDate":"2026-09-12","order":3,"tableOfContents":[{"id":"four-layers","title":"Four layers","level":2},{"id":"the-default-flow","title":"The default flow","level":2},{"id":"breaking-the-stage-lockstep","title":"Breaking the stage lockstep","level":2},{"id":"where-artifacts-and-cache-fit","title":"Where artifacts and cache fit","level":2},{"id":"what-triggers-a-pipeline","title":"What triggers a pipeline","level":2},{"id":"controlling-the-pipeline-as-a-whole","title":"Controlling the pipeline as a whole","level":2},{"id":"putting-the-map-together","title":"Putting the map together","level":2},{"id":"where-to-go-next","title":"Where to go next","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between a pipeline, a stage and a job?","answer":"A job is a single unit of work with its own script, running in its own isolated environment on a runner. A stage is a named group of jobs that execute in parallel with each other. A pipeline is one complete run of all the stages, triggered by an event such as a push, a merge request, a tag or a schedule. Stages run in sequence and, by default, each waits for every job in the previous stage to succeed before starting."},{"question":"Do jobs in the same stage run at the same time?","answer":"Yes, provided runners are available to take them. That is the defining property of a stage. Putting two jobs in test means they run concurrently, and the stage is complete only when both finish. If jobs in a stage appear to run one after another, you have fewer free runners than jobs, or the runner's concurrent setting limits how many it will execute simultaneously."},{"question":"What is the difference between artifacts and cache?","answer":"Artifacts preserve a job's output and are guaranteed to reach the jobs that need them, as well as being downloadable from the GitLab UI. Cache stores reusable inputs such as node_modules to make later runs faster, with no guarantee it will be there. It can be missing on a new branch, stale, or absent because the job ran on a different runner. The test is whether the pipeline would still be correct if the data vanished: if yes, cache; if no, artifacts."},{"question":"Why are two pipelines created every time I push?","answer":"Because both branch pipelines and merge request pipelines are running for the same commit. When a branch has an open merge request, GitLab creates a pipeline for the branch and another for the merge request unless you tell it otherwise. The standard fix is a top-level workflow: rules: block that runs the merge request pipeline when one exists, suppresses the branch pipeline in that case, and always runs on the default branch. Defining it once at the top applies it to the whole pipeline."},{"question":"Why doesn't my rule match on merge requests?","answer":"Because $CICOMMITBRANCH is not set in a merge request pipeline. It only exists in branch pipelines, so a condition written against it silently never matches when the pipeline was triggered by a merge request event. Use $CIMERGEREQUESTSOURCEBRANCHNAME for the branch name in that context, or branch on $CIPIPELINESOURCE == "mergerequest_event" to distinguish the pipeline type explicitly."},{"question":"Can a job start before the previous stage finishes?","answer":"Yes, with needs:. Listing the specific jobs a job depends on lets it begin as soon as those have completed, ignoring stage boundaries and turning the pipeline into a directed acyclic graph. This can cut total runtime substantially when job durations are uneven. A deploy that only needs the build no longer waits for a slow test suite. The cost is that the pipeline becomes harder to read, so it is worth using where the time saving is genuine rather than applying it everywhere."}]},{"slug":"gitlab-ci-stages-jobs","title":"GitLab CI Stages and Jobs","seoTitle":"GitLab CI Stages and Jobs: Syntax, Order and Parallel Jobs","description":"Define GitLab CI stages and jobs: the stages keyword and ordering, job scripts, core job keywords, rules and only/except conditions, and parallel jobs.","content":"$33","difficulty":"Easy","duration":"12 min read","keywords":["gitlab ci stages","gitlab ci jobs","gitlab pipeline stages","gitlab ci script","gitlab ci rules","gitlab ci only except","gitlab ci parallel jobs"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":4,"tableOfContents":[{"id":"what-are-stages-in-gitlab-ci","title":"What Are Stages in GitLab CI?","level":2},{"id":"sequential-execution-between-stages","title":"Sequential execution between stages","level":3},{"id":"default-stages-in-gitlab","title":"Default Stages in GitLab","level":2},{"id":"the-stages-keyword-and-ordering-rules","title":"The stages: Keyword and Ordering Rules","level":2},{"id":"what-are-jobs","title":"What Are Jobs?","level":2},{"id":"core-job-keywords","title":"Core Job Keywords","level":2},{"id":"script","title":"script","level":3},{"id":"before_script","title":"before_script","level":3},{"id":"after_script","title":"after_script","level":3},{"id":"image","title":"image","level":3},{"id":"tags","title":"tags","level":3},{"id":"environment","title":"environment","level":3},{"id":"a-simple-multi-stage-pipeline","title":"A Simple Multi-Stage Pipeline","level":2},{"id":"controlling-when-jobs-run-with-rules","title":"Controlling When Jobs Run with rules","level":2},{"id":"rules-if","title":"rules: if","level":3},{"id":"rules-changes","title":"rules: changes","level":3},{"id":"rules-when","title":"rules: when","level":3},{"id":"rule-order-matters","title":"Rule order matters","level":3},{"id":"only-and-except-older-syntax","title":"only and except: Older Syntax","level":2},{"id":"manual-jobs-with-when-manual","title":"Manual Jobs with when: manual","level":2},{"id":"parallel-jobs","title":"Parallel Jobs","level":2},{"id":"jobs-in-the-same-stage-run-in-parallel","title":"Jobs in the same stage run in parallel","level":3},{"id":"the-parallel-keyword","title":"The parallel keyword","level":3},{"id":"matrix-builds","title":"Matrix builds","level":3},{"id":"job-dependencies-with-needs-for-dag-pipelines","title":"Job Dependencies with needs for DAG Pipelines","level":2},{"id":"why-needs-matters","title":"Why needs matters","level":3},{"id":"complete-realistic-gitlab-ciyml-example-for-a-nodejs-app","title":"Complete Realistic .gitlab-ci.yml Example for a Node.js App","level":2},{"id":"how-this-pipeline-behaves","title":"How this pipeline behaves","level":3},{"id":"practical-tips-for-better-jobs-and-stages","title":"Practical Tips for Better Jobs and Stages","level":2},{"id":"keep-job-names-descriptive","title":"Keep job names descriptive","level":3},{"id":"prefer-many-small-jobs-over-one-giant-job","title":"Prefer many small jobs over one giant job","level":3},{"id":"use-rules-intentionally","title":"Use rules intentionally","level":3},{"id":"use-needs-to-reduce-waiting-not-to-create-confusion","title":"Use needs to reduce waiting, not to create confusion","level":3},{"id":"match-job-images-to-the-task","title":"Match job images to the task","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between a stage and a job in GitLab CI?","answer":"A job is a single unit of work with its own script, its own container and its own runner. A stage is a named group of jobs. Jobs within the same stage run in parallel with one another, and by default a stage does not begin until every job in the previous stage has succeeded. So stages: [build, test, deploy] means all build jobs finish before any test job starts. The needs: keyword breaks that lockstep when you want a job to start as soon as its own dependency is ready."},{"question":"What is the difference between rules, only and except?","answer":"only and except are the older syntax and are no longer recommended, though they still work. rules: replaced them and is considerably more capable: it evaluates conditions in order, stops at the first match, and can set when, allow_failure and variables per rule. A practical difference that catches people is that rules and only/except cannot be used in the same job. GitLab rejects the pipeline. New pipelines should use rules exclusively."},{"question":"Why do my jobs run in the wrong order?","answer":"Almost always a stage that is not declared in the stages: list, or jobs assigned to stages in an order different from the one you expect. Job execution order comes entirely from stage order, not from the order jobs appear in the file. A job with no stage: key is placed in test by default, which frequently puts it earlier or later than intended. If you want ordering based on actual data dependencies rather than stages, use needs: to build a directed acyclic graph."},{"question":"What does needs: do and when should I use it?","answer":"needs: lets a job start as soon as the specific jobs it names have finished, instead of waiting for the whole previous stage. That turns a strictly sequential pipeline into a DAG and can cut total runtime substantially when some branches are much slower than others. It also implicitly fetches artifacts from the jobs it names. The trade-off is that the pipeline graph becomes harder to reason about, so it is worth applying where the time saving is real rather than everywhere by default."},{"question":"How do I make a job manual?","answer":"Set when: manual on the job, which adds a play button in the pipeline view and leaves the job waiting until someone clicks it. This is the standard way to gate a production deployment behind human approval. By default a manual job is optional and the pipeline can succeed without it; adding allow_failure: false makes the pipeline block until it is run, which is what you want when the manual step is genuinely required rather than a convenience."},{"question":"Why is my job not running at all?","answer":"Either a rules, only or except condition excluded it, or it is waiting for a runner. Open the pipeline and check whether the job appears at all. A job filtered out by rules is absent from the pipeline entirely, while a job waiting for a runner shows as pending. For rules problems, the usual cause is a branch or pipeline-source condition that does not match: a rule written for $CICOMMITBRANCH == "main" never fires on a merge request pipeline, where the branch variable is not set and $CIMERGEREQUESTSOURCEBRANCH_NAME applies instead."}]},{"slug":"gitlab-ci-artifacts","title":"GitLab CI Artifacts","seoTitle":"GitLab CI Artifacts: Pass Files Between Jobs and Download","description":"Use GitLab CI artifacts to pass files between jobs and keep build output: set paths and expire_in, download from the UI, and compare dependencies with needs.","content":"$34","difficulty":"Easy","duration":"10 min read","keywords":["gitlab ci artifacts","gitlab pipeline artifacts","gitlab artifacts download","gitlab ci artifacts expire in","gitlab ci reports","pass files between jobs gitlab"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":5,"tableOfContents":[{"id":"what-are-artifacts","title":"What Are Artifacts?","level":2},{"id":"defining-artifacts-with-artifacts-paths","title":"Defining Artifacts with artifacts: paths:","level":2},{"id":"what-paths-means","title":"What paths means","level":3},{"id":"why-this-matters","title":"Why this matters","level":3},{"id":"artifacts-expire_in-for-automatic-cleanup","title":"artifacts: expire_in: for Automatic Cleanup","level":2},{"id":"common-expiration-choices","title":"Common expiration choices","level":3},{"id":"why-expiration-is-important","title":"Why expiration is important","level":3},{"id":"downloading-artifacts-from-the-gitlab-ui","title":"Downloading Artifacts from the GitLab UI","level":2},{"id":"passing-files-to-later-jobs","title":"Passing Files to Later Jobs","level":2},{"id":"accessing-artifacts-from-previous-jobs-dependencies-vs-needs","title":"Accessing Artifacts from Previous Jobs: dependencies vs needs","level":2},{"id":"using-dependencies","title":"Using dependencies","level":3},{"id":"using-needs","title":"Using needs","level":3},{"id":"when-to-use-which","title":"When to use which","level":3},{"id":"artifacts-reports-for-structured-gitlab-reports","title":"artifacts: reports: for Structured GitLab Reports","level":2},{"id":"common-report-artifact-types","title":"Common report artifact types","level":3},{"id":"why-reports-are-special","title":"Why reports are special","level":3},{"id":"artifacts-when","title":"artifacts: when:","level":2},{"id":"on_success","title":"on_success","level":3},{"id":"on_failure","title":"on_failure","level":3},{"id":"always","title":"always","level":3},{"id":"artifacts-untracked","title":"artifacts: untracked:","level":2},{"id":"why-use-it-carefully","title":"Why use it carefully","level":3},{"id":"real-example-1-build-artifact-for-a-compiled-application","title":"Real Example 1: Build Artifact for a Compiled Application","level":2},{"id":"real-example-2-test-report-artifact","title":"Real Example 2: Test Report Artifact","level":2},{"id":"real-example-3-deployment-package-artifact","title":"Real Example 3: Deployment Package Artifact","level":2},{"id":"good-artifact-hygiene","title":"Good Artifact Hygiene","level":2},{"id":"save-what-matters-not-everything","title":"Save what matters, not everything","level":3},{"id":"set-expiration-times","title":"Set expiration times","level":3},{"id":"use-reports-where-possible","title":"Use reports where possible","level":3},{"id":"avoid-rebuilding-if-a-verified-build-output-already-exists","title":"Avoid rebuilding if a verified build output already exists","level":3},{"id":"common-mistakes","title":"Common Mistakes","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between artifacts and cache in GitLab CI?","answer":"Artifacts preserve job output that matters to the result, compiled binaries, test reports, deployment bundles, and are passed to later jobs and downloadable from the UI. Cache stores reusable inputs such as node_modules or a pip download directory to make future runs faster. The distinction that matters is correctness: artifacts are guaranteed to be there, while a cache may be missing, stale or from another branch, so a pipeline must never depend on cache contents to produce a correct result."},{"question":"How do I pass files from one job to another?","answer":"Declare them as artifacts in the producing job with artifacts: paths:, and by default every job in a later stage downloads them automatically. To control that precisely, use needs: to name the specific jobs whose artifacts you want, or dependencies: to restrict it, dependencies: [] downloads nothing at all, which is worth setting on jobs that do not need them since it speeds them up noticeably."},{"question":"How long are artifacts kept?","answer":"Until their expirein elapses, defaulting to 30 days on GitLab.com unless the instance or project overrides it. Set expirein explicitly on every artifact: an hour is plenty for files only passed between jobs in the same pipeline, while release builds might justify weeks. Artifacts consume project storage and are a common cause of unexpectedly hitting a storage quota, since a pipeline running dozens of times a day accumulates quickly. The latest artifacts for the most recent successful pipeline on a ref are kept regardless unless you disable that behaviour."},{"question":"What is artifacts: reports: for?","answer":"It tells GitLab that an artifact is a structured report of a known type, JUnit test results, code coverage, SAST findings, a dotenv file, so the platform can parse it and surface the contents in the merge request rather than leaving it as a file to download. A JUnit report turns into a test summary showing exactly which tests newly failed, which is far more useful than reading a log. Report artifacts are subject to their own retention rules and, for some types, size limits."},{"question":"Why are my artifacts empty or missing?","answer":"Usually the paths are wrong relative to the project root, which is where they are evaluated from, not from wherever the script happened to cd. The job log lists which files were collected, so check there first. The other frequent cause is that the job failed before producing them: by default artifacts are only uploaded when a job succeeds, so if you want them from failures too, for example to capture logs or screenshots, set artifacts: when: always."},{"question":"Can I download artifacts without using the UI?","answer":"Yes. The GitLab API exposes them at a per-job endpoint, and there is a stable URL for the latest artifacts from a successful job on a given ref, which is what most external tooling uses. A CI job token authorises access from within another pipeline, which is how multi-project pipelines consume builds from an upstream project without a personal access token."}]},{"slug":"gitlab-ci-cache","title":"GitLab CI Cache","seoTitle":"GitLab CI Cache: Speed Up Pipelines with Cache Keys","description":"Speed up GitLab CI with caching: define cache paths and keys, use cache policies, see how cache differs from artifacts and cache node_modules and pip.","content":"$35","difficulty":"Easy","duration":"10 min read","keywords":["gitlab ci cache","gitlab pipeline cache","gitlab ci cache key","gitlab ci node_modules cache","gitlab ci npm cache","speed up gitlab pipeline","gitlab ci cache policy"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":6,"tableOfContents":[{"id":"what-is-caching","title":"What Is Caching?","level":2},{"id":"cache-vs-artifacts","title":"Cache vs Artifacts","level":2},{"id":"defining-cache-with-cache-paths","title":"Defining Cache with cache: paths:","level":2},{"id":"what-should-go-into-paths","title":"What should go into paths","level":3},{"id":"cache-keys-with-cache-key","title":"Cache Keys with cache: key:","level":2},{"id":"why-keys-matter","title":"Why keys matter","level":3},{"id":"branch-based-key-strategy","title":"Branch-based key strategy","level":3},{"id":"file-based-key-strategy","title":"File-based key strategy","level":3},{"id":"using-ci_commit_ref_slug","title":"Using $CI_COMMIT_REF_SLUG","level":2},{"id":"cache-policies","title":"Cache Policies","level":2},{"id":"default-pull-push","title":"Default pull-push","level":3},{"id":"pull","title":"pull","level":3},{"id":"push","title":"push","level":3},{"id":"cache-when","title":"Cache when:","level":2},{"id":"on_success","title":"on_success","level":3},{"id":"on_failure","title":"on_failure","level":3},{"id":"always","title":"always","level":3},{"id":"common-caching-example-npm-and-node_modules","title":"Common Caching Example: npm and node_modules","level":2},{"id":"example-using-npm-cache-directory","title":"Example using npm cache directory","level":3},{"id":"example-caching-node_modules","title":"Example caching node_modules/","level":3},{"id":"common-caching-example-pip-and-python-environments","title":"Common Caching Example: pip and Python Environments","level":2},{"id":"cache-pip-downloads","title":"Cache pip downloads","level":3},{"id":"cache-a-virtual-environment","title":"Cache a virtual environment","level":3},{"id":"common-caching-example-maven","title":"Common Caching Example: Maven","level":2},{"id":"common-caching-example-gradle","title":"Common Caching Example: Gradle","level":2},{"id":"fallback-keys","title":"Fallback Keys","level":2},{"id":"cache-invalidation-strategies","title":"Cache Invalidation Strategies","level":2},{"id":"strategy-1-lock-file-based-invalidation","title":"Strategy 1: Lock-file based invalidation","level":3},{"id":"strategy-2-branch-based-isolation","title":"Strategy 2: Branch-based isolation","level":3},{"id":"strategy-3-versioned-keys","title":"Strategy 3: Versioned keys","level":3},{"id":"strategy-4-fallback-hierarchy","title":"Strategy 4: Fallback hierarchy","level":3},{"id":"artifacts-vs-cache-quick-comparison","title":"Artifacts vs Cache: Quick Comparison","level":2},{"id":"practical-advice-for-better-caching","title":"Practical Advice for Better Caching","level":2},{"id":"cache-less-but-cache-smart","title":"Cache less, but cache smart","level":3},{"id":"do-not-trust-cache-for-correctness","title":"Do not trust cache for correctness","level":3},{"id":"avoid-over-sharing-stale-caches","title":"Avoid over-sharing stale caches","level":3},{"id":"measure-before-and-after","title":"Measure before and after","level":3},{"id":"common-mistakes","title":"Common Mistakes","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between cache and artifacts?","answer":"Cache speeds up future pipeline runs by reusing inputs such as node_modules, a pip download directory or a Maven repository. Artifacts preserve a job's output so later jobs and humans can use it. The practical rule is that cache is an optimisation and artifacts are a guarantee: a pipeline must still produce correct results if every cache is empty, because a cache can be missing, expired or belong to a different branch. Anything a later job genuinely requires belongs in artifacts."},{"question":"Why is my cache not being restored?","answer":"Most often the key does not match. A cache is only restored when a job's key resolves to exactly the same value that stored it, so a key built from a lockfile hash changes the moment that file changes, which is intended, but surprises people. Check the job log, which states whether it found a cache and under which key. The other common causes are caching a path outside the project directory, which is not supported, and jobs running on different runners where the cache is stored locally rather than in shared object storage."},{"question":"How do I cache node_modules correctly?","answer":"Key the cache on the lockfile so it invalidates when dependencies change, and prefer caching the package manager's download directory over nodemodules itself. Restoring a download cache and running npm ci is more reliable than restoring a partially-populated nodemodules, particularly across different Node versions or base images. Add a fallback_keys entry pointing at a branch-level key so a new lockfile still gets a partial warm start instead of downloading everything from scratch."},{"question":"What do the cache policies do?","answer":"pull-push is the default and means the job restores the cache at the start and uploads it at the end. pull restores without uploading, which is right for jobs that only consume dependencies and would otherwise waste time re-uploading an unchanged cache. push uploads without restoring, useful for a dedicated warming job that builds the cache once at the start of a pipeline. Setting pull on the many jobs that only read is one of the easiest pipeline speed-ups available."},{"question":"Should I use cache for build output?","answer":"No. Build output belongs in artifacts. Cache has no delivery guarantee, so a job that depends on cached build output will work most of the time and then fail confusingly when the cache is missing or stale, typically on a new branch, or after a runner change. Use cache strictly for things that can be regenerated safely from scratch, and artifacts for anything a later job needs to be certain about."},{"question":"Why did caching make my pipeline slower?","answer":"Because uploading and downloading a large cache can cost more than regenerating its contents. A multi-gigabyte cache over a slow link easily exceeds the time saved, especially on a fast dependency install. Cache selectively rather than broadly, a package manager's download directory rather than an entire workspace, and measure the job duration before and after. Setting policy: pull on jobs that do not change the cache removes half the transfer cost immediately."}]},{"slug":"gitlab-ci-variables-and-secrets","title":"Variables, Secrets and Environments in GitLab CI","seoTitle":"GitLab CI Variables and Secrets: Masked and Protected","description":"Use GitLab CI/CD variables safely: predefined and custom variables, masked and protected values, group-level and scoped variables, and precedence rules.","content":"$36","difficulty":"Easy","duration":"12 min read","keywords":["gitlab ci variables","gitlab ci secrets","gitlab ci masked variables","gitlab ci protected variables","gitlab cicd environment variables","gitlab ci predefined variables","gitlab ci group variables","gitlab ci variable scoping","gitlab ci secrets management","gitlab ci .env file"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":7,"tableOfContents":[{"id":"what-are-cicd-variables-and-why-do-they-matter","title":"What Are CI/CD Variables and Why Do They Matter?","level":2},{"id":"common-things-variables-store","title":"Common things variables store","level":3},{"id":"why-hard-coding-is-a-bad-habit","title":"Why hard-coding is a bad habit","level":3},{"id":"predefined-variables-in-gitlab-ci","title":"Predefined Variables in GitLab CI","level":2},{"id":"ci_commit_branch","title":"CI_COMMIT_BRANCH","level":3},{"id":"ci_commit_sha","title":"CI_COMMIT_SHA","level":3},{"id":"ci_project_name","title":"CI_PROJECT_NAME","level":3},{"id":"ci_pipeline_id","title":"CI_PIPELINE_ID","level":3},{"id":"ci_registry","title":"CI_REGISTRY","level":3},{"id":"ci_environment_name","title":"CI_ENVIRONMENT_NAME","level":3},{"id":"why-predefined-variables-are-powerful","title":"Why predefined variables are powerful","level":3},{"id":"defining-custom-variables-in-gitlab-ciyml","title":"Defining Custom Variables in .gitlab-ci.yml","level":2},{"id":"global-variables","title":"Global variables","level":3},{"id":"job-level-variables","title":"Job-level variables","level":3},{"id":"when-yaml-defined-variables-are-appropriate","title":"When YAML-defined variables are appropriate","level":3},{"id":"defining-variables-in-project-settings","title":"Defining Variables in Project Settings","level":2},{"id":"group-level-variables","title":"Group-Level Variables","level":2},{"id":"why-group-level-variables-are-useful","title":"Why group-level variables are useful","level":3},{"id":"benefits","title":"Benefits","level":3},{"id":"variable-precedence-which-value-wins","title":"Variable Precedence: Which Value Wins?","level":2},{"id":"why-this-matters-in-practice","title":"Why this matters in practice","level":3},{"id":"best-practice-for-sanity","title":"Best practice for sanity","level":3},{"id":"masked-variables","title":"Masked Variables","level":2},{"id":"why-masking-matters","title":"Why masking matters","level":3},{"id":"important-masked-variable-requirements","title":"Important masked variable requirements","level":3},{"id":"very-important-limitation","title":"Very important limitation","level":3},{"id":"protected-variables","title":"Protected Variables","level":2},{"id":"why-protected-variables-matter","title":"Why protected variables matter","level":3},{"id":"typical-use-cases","title":"Typical use cases","level":3},{"id":"masked-protected-the-best-combination-for-production-secrets","title":"Masked + Protected: The Best Combination for Production Secrets","level":2},{"id":"environment-scoped-variables","title":"Environment-Scoped Variables","level":2},{"id":"the-core-idea","title":"The core idea","level":3},{"id":"why-this-is-elegant","title":"Why this is elegant","level":3},{"id":"example-mental-model","title":"Example mental model","level":3},{"id":"real-world-value","title":"Real-world value","level":3},{"id":"referencing-variables-in-scripts","title":"Referencing Variables in Scripts","level":2},{"id":"linux-and-unix-style-shells","title":"Linux and Unix-style shells","level":3},{"id":"windows-shells","title":"Windows shells","level":3},{"id":"why-syntax-awareness-matters","title":"Why syntax awareness matters","level":3},{"id":"the-dotenv-artifact-passing-variables-between-jobs","title":"The Dotenv Artifact: Passing Variables Between Jobs","level":2},{"id":"the-problem-it-solves","title":"The problem it solves","level":3},{"id":"the-gitlab-solution","title":"The GitLab solution","level":3},{"id":"why-this-is-better-than-hacks","title":"Why this is better than hacks","level":3},{"id":"file-type-variables","title":"File-Type Variables","level":2},{"id":"how-file-type-variables-work","title":"How file-type variables work","level":3},{"id":"why-they-matter","title":"Why they matter","level":3},{"id":"a-complete-example-managing-database-urls-api-keys-and-deploy-tokens-properly","title":"A Complete Example: Managing Database URLs, API Keys, and Deploy Tokens Properly","level":2},{"id":"in-gitlab-ciyml","title":"In .gitlab-ci.yml","level":3},{"id":"in-project-or-group-settings","title":"In project or group settings","level":3},{"id":"why-this-setup-is-strong","title":"Why this setup is strong","level":3},{"id":"security-best-practices-for-gitlab-ci-secrets","title":"Security Best Practices for GitLab CI Secrets","level":2},{"id":"never-echo-secrets","title":"Never echo secrets","level":3},{"id":"use-masked-variables-for-sensitive-strings","title":"Use masked variables for sensitive strings","level":3},{"id":"use-protected-variables-for-production-credentials","title":"Use protected variables for production credentials","level":3},{"id":"rotate-secrets-regularly","title":"Rotate secrets regularly","level":3},{"id":"prefer-least-privilege","title":"Prefer least privilege","level":3},{"id":"avoid-storing-secrets-in-the-repository","title":"Avoid storing secrets in the repository","level":3},{"id":"use-file-type-variables-for-structured-multi-line-credentials","title":"Use file-type variables for structured multi-line credentials","level":3},{"id":"review-who-can-trigger-protected-pipelines","title":"Review who can trigger protected pipelines","level":3},{"id":"common-beginner-mistakes-with-variables","title":"Common Beginner Mistakes with Variables","level":2},{"id":"mistake-1-using-yaml-variables-for-secrets","title":"Mistake 1: Using YAML variables for secrets","level":3},{"id":"mistake-2-forgetting-environment-scopes","title":"Mistake 2: Forgetting environment scopes","level":3},{"id":"mistake-3-assuming-masking-makes-logging-safe","title":"Mistake 3: Assuming masking makes logging safe","level":3},{"id":"mistake-4-reusing-one-powerful-token-everywhere","title":"Mistake 4: Reusing one powerful token everywhere","level":3},{"id":"mistake-5-ignoring-precedence-conflicts","title":"Mistake 5: Ignoring precedence conflicts","level":3},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Where should I store secrets in GitLab CI?","answer":"In project, group or instance CI/CD variables, marked both masked and protected, never in .gitlab-ci.yml, which is committed to the repository and visible to anyone with read access. Masking keeps the value out of job logs and protection restricts it to protected branches and tags, so a merge request from a fork cannot read your production credentials. For higher-value secrets, GitLab integrates with external managers such as HashiCorp Vault so the pipeline fetches a short-lived credential at run time rather than holding a long-lived one."},{"question":"What is the difference between masked and protected variables?","answer":"Masking replaces the value with [MASKED] wherever it would appear in a job log, which prevents accidental disclosure through debug output. Protection restricts the variable to jobs running on protected branches and protected tags, so ordinary feature branches and merge requests never receive it. They solve different problems and production secrets should have both: masking alone still exposes the value to any branch, and protection alone still risks printing it into a log."},{"question":"Why is my masked variable not being masked?","answer":"GitLab can only mask values that meet its requirements: a minimum length, a single line, and no characters outside the supported set. A value with spaces, newlines or unusual punctuation is rejected for masking, and GitLab tells you so when you save it rather than silently failing. Base64-encoding an awkward value is the usual workaround. Note that masking only covers exact matches: if a script transforms the secret before printing it, the transformed form appears in the log unmasked."},{"question":"Which variable wins when the same name is defined in several places?","answer":"The most specific definition wins. Roughly, from lowest to highest precedence: instance variables, then group, then project, then variables defined in .gitlab-ci.yml at the top level, then job-level variables, and finally values passed when manually triggering a pipeline or by a trigger token. Predefined variables that GitLab supplies cannot be overridden. When a value is not what you expect, printing it in a throwaway job is the quickest way to confirm which layer supplied it."},{"question":"How do I pass a variable from one job to another?","answer":"Write it to a dotenv file and expose that file as a dotenv report artifact. Variables in it are then automatically available as environment variables in subsequent jobs, which is the supported way to hand a computed value, a generated image tag, a deployment URL, a version number, down the pipeline. Plain export does not work, because each job runs in its own container and shell with no shared state."},{"question":"Can merge requests from forks access my secrets?","answer":"Not if the variables are marked protected, which is exactly what protection is for. An unprotected variable is available to any pipeline including those from forked repositories, which means an untrusted contributor could add a job that prints it. This is the single most important reason to mark production credentials protected as well as masked, and to review changes to .gitlab-ci.yml in merge requests from outside your organisation with particular care."}]},{"slug":"gitlab-runners","title":"GitLab Runners","seoTitle":"GitLab Runner Setup: Types, Executors and Registration","description":"Learn what a GitLab Runner is and how it runs jobs: shared versus specific runners, executors, installing and registering on Ubuntu, and a Docker executor.","content":"$37","difficulty":"Easy","duration":"12 min read","keywords":["gitlab runner","gitlab ci runner","gitlab runner types","gitlab runner registration","gitlab runner executor","shared runner gitlab","specific runner gitlab","gitlab runner docker executor","install gitlab runner"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":8,"tableOfContents":[{"id":"what-is-a-gitlab-runner-and-why-it-exists","title":"What Is a GitLab Runner and Why It Exists","level":2},{"id":"types-of-gitlab-runners","title":"Types of GitLab Runners","level":2},{"id":"shared-runners","title":"Shared runners","level":3},{"id":"group-runners","title":"Group runners","level":3},{"id":"project-specific-runners","title":"Project-specific runners","level":3},{"id":"gitlab-runner-executor-types","title":"GitLab Runner Executor Types","level":2},{"id":"shell-executor","title":"Shell executor","level":3},{"id":"docker-executor","title":"Docker executor","level":3},{"id":"dockermachine-executor","title":"Docker+Machine executor","level":3},{"id":"kubernetes-executor","title":"Kubernetes executor","level":3},{"id":"ssh-executor","title":"SSH executor","level":3},{"id":"how-the-docker-executor-works","title":"How the Docker Executor Works","level":2},{"id":"installing-gitlab-runner-on-ubuntu-or-linux","title":"Installing GitLab Runner on Ubuntu or Linux","level":2},{"id":"1-update-package-indexes","title":"1. Update package indexes","level":3},{"id":"2-add-the-gitlab-runner-repository","title":"2. Add the GitLab Runner repository","level":3},{"id":"3-install-the-runner-package","title":"3. Install the runner package","level":3},{"id":"4-verify-the-installation","title":"4. Verify the installation","level":3},{"id":"registering-a-runner-with-gitlab-runner-register","title":"Registering a Runner with gitlab-runner register","level":2},{"id":"understanding-configtoml","title":"Understanding config.toml","level":2},{"id":"using-tags-to-route-jobs","title":"Using Tags to Route Jobs","level":2},{"id":"runner-security-best-practices","title":"Runner Security Best Practices","level":2},{"id":"why-not-use-shared-runners-for-production-secrets","title":"Why not use shared runners for production secrets?","level":3},{"id":"protected-runners","title":"Protected runners","level":3},{"id":"protected-variables","title":"Protected variables","level":3},{"id":"runner-concurrency-settings","title":"Runner Concurrency Settings","level":2},{"id":"complete-example-docker-executor-runner-and-matching-pipeline","title":"Complete Example: Docker Executor Runner and Matching Pipeline","level":2},{"id":"register-the-runner","title":"Register the runner","level":3},{"id":"resulting-runner-intent","title":"Resulting runner intent","level":3},{"id":"gitlab-ciyml","title":".gitlab-ci.yml","level":3},{"id":"common-mistakes-to-avoid","title":"Common Mistakes to Avoid","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a GitLab Runner?","answer":"It is the agent that actually executes your jobs. GitLab itself only orchestrates. It decides which jobs should run and in what order, then hands each one to a runner, which creates the environment, runs the script and streams the log back. Without at least one available runner, pipelines are created but every job sits pending forever. Runners can be shared across an instance, assigned to a group, or dedicated to a single project."},{"question":"What is the difference between the runner executors?","answer":"The executor decides where a job's script runs. docker starts a fresh container per job from the image the job specifies, which gives clean isolation and is the usual choice. shell runs commands directly on the runner host, which is simple but leaves state behind between jobs and is a security concern on shared runners. kubernetes schedules each job as a Pod, which suits teams already running a cluster. docker-machine for autoscaling on cloud VMs is deprecated in favour of the newer autoscaling approach."},{"question":"Why are my jobs stuck in pending?","answer":"No runner matches the job. The usual causes are a tags: value in the job that no registered runner carries, shared runners being disabled for the project, all runners being busy at their concurrency limit, or the runner being offline. The job page states which applies. Tag mismatches are the most common by a distance. Tags are exact-match strings, and a job tagged docker-builder will never run on a runner registered as docker_builder."},{"question":"How do I install and register a runner?","answer":"Install the package from GitLab's repository for your distribution, then run gitlab-runner register and supply the GitLab URL, a registration token from the project or group's CI/CD settings, an executor, and a default image if using Docker. Registration writes an entry into config.toml, which is where you later adjust concurrency, volumes and cache settings. Modern GitLab uses runner authentication tokens created in the UI rather than the older shared registration token, so follow the flow your version presents."},{"question":"What does concurrent in config.toml control?","answer":"The total number of jobs the runner process will execute simultaneously across all its registered runners. It is a global setting at the top of config.toml, separate from the per-runner limit. The default of 1 means jobs queue one at a time, which surprises teams who add a powerful machine and see no throughput improvement. Raise it to something the host can genuinely support. Bearing in mind each concurrent Docker job needs its own CPU, memory and disk for the image and build directory."},{"question":"Are self-hosted runners safe to share between projects?","answer":"Only with care. A runner using the shell executor gives every job full access to the host, so any project that can run jobs on it can read files, credentials and other projects' build directories. The docker executor is much better isolated, but privileged mode, often enabled to allow Docker-in-Docker image builds, effectively grants root on the host and undoes that isolation. For untrusted code, or for repositories that accept outside contributions, use ephemeral runners that are destroyed after each job."}]},{"slug":"gitlab-ci-pipeline-rules","title":"GitLab CI Pipeline Rules","seoTitle":"GitLab CI rules: when jobs run, and when they do not","description":"Control exactly when GitLab CI jobs run with rules:if, rules:changes and rules:exists, plus workflow rules, merge request pipelines and manual jobs.","content":"$38","difficulty":"Easy","duration":"12 min read","keywords":["gitlab ci rules","gitlab ci rules if","gitlab ci rules changes","gitlab pipeline conditions","gitlab ci when manual","gitlab ci only except","gitlab ci rules exists","gitlab pipeline trigger conditions","gitlab ci merge request pipeline"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":9,"tableOfContents":[{"id":"why-controlling-when-jobs-run-matters","title":"Why Controlling When Jobs Run Matters","level":2},{"id":"rules-vs-the-older-onlyexcept","title":"rules vs the Older only/except","level":2},{"id":"older-pattern-with-only","title":"Older pattern with only","level":3},{"id":"recommended-pattern-with-rules","title":"Recommended pattern with rules","level":3},{"id":"rulesif-for-variable-based-conditions","title":"rules:if for Variable-Based Conditions","level":2},{"id":"common-ci-variables","title":"Common CI variables","level":3},{"id":"example-run-only-on-main-branch","title":"Example: run only on main branch","level":3},{"id":"example-run-only-on-merge-requests","title":"Example: run only on merge requests","level":3},{"id":"example-skip-on-tags","title":"Example: skip on tags","level":3},{"id":"example-target-by-pipeline-source","title":"Example: target by pipeline source","level":3},{"id":"ruleschanges-for-file-based-execution","title":"rules:changes for File-Based Execution","level":2},{"id":"example-with-multiple-paths","title":"Example with multiple paths","level":3},{"id":"rulesexists-for-repository-aware-jobs","title":"rules:exists for Repository-Aware Jobs","level":2},{"id":"ruleswhen-and-job-behavior","title":"rules:when and Job Behavior","level":2},{"id":"manual-job-example","title":"Manual job example","level":3},{"id":"failure-handling-example","title":"Failure-handling example","level":3},{"id":"delayed-job-example","title":"Delayed job example","level":3},{"id":"rulesallow_failure","title":"rules:allow_failure","level":2},{"id":"manual-jobs-with-when-manual-and-allow_failure-true","title":"Manual Jobs with when: manual and allow_failure: true","level":2},{"id":"combining-multiple-rules","title":"Combining Multiple Rules","level":2},{"id":"or-logic-between-rule-items","title":"OR logic between rule items","level":3},{"id":"and-logic-within-one-rule-item","title":"AND logic within one rule item","level":3},{"id":"rule-ordering-matters","title":"Rule ordering matters","level":3},{"id":"merge-request-pipelines-vs-branch-pipelines","title":"Merge Request Pipelines vs Branch Pipelines","level":2},{"id":"workflowrules-for-controlling-the-entire-pipeline","title":"workflow:rules for Controlling the Entire Pipeline","level":2},{"id":"complete-practical-example-mr-vs-main-branch","title":"Complete Practical Example: MR vs Main Branch","level":2},{"id":"common-rule-design-mistakes","title":"Common Rule Design Mistakes","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do rules work in GitLab CI?","answer":"Each job's rules: list is evaluated top to bottom and the first matching rule wins. Evaluation stops there, and later rules are never considered. The matched rule determines whether the job is added to the pipeline and with what when, allow_failure and variables. If no rule matches, the job is excluded from the pipeline entirely rather than failing. Because of the first-match behaviour, ordering rules from most specific to most general is essential; a broad catch-all placed first makes everything below it dead code."},{"question":"Why is my job not appearing in the pipeline?","answer":"No rule matched, so the job was never created. This looks different from a failure. The job is absent from the pipeline graph rather than shown as skipped or failed. The most frequent cause is a branch condition that does not apply to the pipeline type: $CICOMMITBRANCH is not set in merge request pipelines, so a rule written against it silently never matches there. Use $CIMERGEREQUESTSOURCEBRANCHNAME or a $CIPIPELINESOURCE == "mergerequest_event" condition for those."},{"question":"What is the difference between rules:if and rules:changes?","answer":"if evaluates an expression against CI/CD variables: the branch name, the pipeline source, a custom variable. changes matches when specific files were modified in the commit, which is how you run a job only when the relevant part of a monorepo changed. They can be combined in a single rule, and both must be satisfied for it to match. A caution with changes: on pipelines where GitLab cannot compute a meaningful diff, such as the first pipeline on a new branch, it can evaluate as true and run jobs you expected to be skipped."},{"question":"How do I stop duplicate pipelines running for branches and merge requests?","answer":"This happens when a project has both branch pipelines and merge request pipelines enabled without rules that distinguish them, so each push creates two. The standard fix is workflow-level rules that run a merge request pipeline when one exists, run a branch pipeline only when no merge request is open for that branch, and always run on the default branch. Putting this in a top-level workflow: rules: block applies it to the whole pipeline rather than repeating the logic on every job."},{"question":"What does when: never do?","answer":"It excludes the job when that rule matches, which is how you carve exceptions out of a broader condition. A common pattern is a first rule with when: never for the cases you want to skip, draft merge requests, documentation-only changes, followed by a general rule that runs the job otherwise. Because evaluation stops at the first match, the exclusion must come before the rule it is excepting from, which is the most frequent mistake with this keyword."},{"question":"Can I use rules and only/except in the same job?","answer":"No. GitLab rejects the pipeline with a configuration error if a job contains both. They are alternative mechanisms, and rules is the current one, only and except still work for backwards compatibility but receive no new features. When migrating, do it a whole job at a time rather than mixing, and remember that rules defaults differ: a job with rules and no matching entry is not created at all, where only had a more permissive default."}]},{"slug":"gitlab-ci-environments","title":"GitLab CI Environments","seoTitle":"GitLab CI Environments: Review Apps and Deployment Tracking","description":"Track deployments with GitLab CI environments: the environment keyword, static versus dynamic environments, review apps, protected environments and approvals.","content":"$39","difficulty":"Medium","duration":"12 min read","keywords":["gitlab ci environments","gitlab ci deployment","gitlab environment tracking","gitlab review apps","gitlab ci deploy environment","gitlab environment url","gitlab protected environments","gitlab deployment approval"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":10,"tableOfContents":[{"id":"what-are-gitlab-environments","title":"What Are GitLab Environments?","level":2},{"id":"the-environment-keyword-in-jobs","title":"The environment Keyword in Jobs","level":2},{"id":"name","title":"name","level":3},{"id":"url","title":"url","level":3},{"id":"static-vs-dynamic-environments","title":"Static vs Dynamic Environments","level":2},{"id":"static-environments","title":"Static environments","level":3},{"id":"dynamic-environments","title":"Dynamic environments","level":3},{"id":"viewing-deployment-history-in-gitlab","title":"Viewing Deployment History in GitLab","level":2},{"id":"dynamic-environments-for-merge-requests","title":"Dynamic Environments for Merge Requests","level":2},{"id":"review-apps-in-gitlab-cicd","title":"Review Apps in GitLab CI/CD","level":2},{"id":"the-on_stop-keyword-and-stop-job-pattern","title":"The on_stop Keyword and Stop Job Pattern","level":2},{"id":"deployment-job","title":"Deployment job","level":3},{"id":"stop-job","title":"Stop job","level":3},{"id":"protected-environments-and-deployment-approvals","title":"Protected Environments and Deployment Approvals","level":2},{"id":"why-protected-environments-matter","title":"Why protected environments matter","level":3},{"id":"deployment-frequency-and-dora-metrics","title":"Deployment Frequency and DORA Metrics","level":2},{"id":"environment-scoped-variables","title":"Environment-Scoped Variables","level":2},{"id":"complete-example-dev-to-staging-to-production","title":"Complete Example: Dev to Staging to Production","level":2},{"id":"what-this-pipeline-does","title":"What this pipeline does","level":3},{"id":"practical-tips-for-designing-environments","title":"Practical Tips for Designing Environments","level":2},{"id":"common-mistakes-to-avoid","title":"Common Mistakes to Avoid","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a GitLab environment and why do I need one?","answer":"An environment is a named deployment target, staging, production, or a per-branch review app, that GitLab tracks over time. Adding an environment: key to a deploy job gives you a deployment history showing what was deployed where and by whom, a link straight to the running application, the ability to protect the target so only certain people can deploy to it, environment-scoped variables, and the data behind DORA metrics. Without it a deploy job is just a script and none of that exists."},{"question":"What is the difference between a static and a dynamic environment?","answer":"A static environment has a fixed name, such as production, and is deployed to repeatedly. A dynamic environment has a name built from CI variables, typically review/$CICOMMITREFSLUG, so every branch gets its own. Dynamic environments are how review apps work: each merge request deploys a live copy reviewers can click through. Because they are created per branch, they need an onstop job to tear them down or they accumulate indefinitely."},{"question":"How do I clean up review apps automatically?","answer":"Pair the deploy job with a stop job and reference it via environment: onstop:. GitLab runs the stop job when the branch is deleted or the merge request is merged or closed, which is what destroys the deployed resources. Setting autostopin adds a time-based expiry as a safety net, autostop_in: 1 week stops environments that were forgotten. Without both, abandoned review apps keep consuming infrastructure and cost money quietly."},{"question":"How do I require approval before deploying to production?","answer":"Use a protected environment. In the project's CI/CD settings, protect the production environment and specify which users or groups are allowed to deploy to it, optionally requiring a number of approvals before the job runs. This is enforced by GitLab rather than by convention, so an ordinary contributor cannot deploy even by editing the pipeline. Combining it with when: manual on the job gives you both an explicit gate and an authorisation check."},{"question":"What are environment-scoped variables?","answer":"CI/CD variables can be limited to a specific environment, so DATABASE_URL resolves to different values in staging and production while the pipeline uses one variable name. The job receives the value matching the environment it declares. This removes the need for branching logic or separately named variables per environment, and it keeps production credentials out of jobs that deploy anywhere else."},{"question":"Do environments cost anything or affect DORA metrics?","answer":"Environments themselves are a free GitLab feature; you only pay for the infrastructure you deploy to. They are also what makes DORA metrics work. Deployment frequency and lead time for changes are both derived from deployments recorded against environments, so a team that deploys without declaring an environment will see those metrics stay empty regardless of how often they actually ship."}]},{"slug":"gitlab-ci-docker","title":"GitLab CI Docker Workflows","seoTitle":"GitLab CI Docker Build and Push: DinD vs Kaniko","description":"Build and push Docker images in GitLab CI: compare Docker-in-Docker with Kaniko, authenticate to the Container Registry, pick tags and follow a full example.","content":"$3a","difficulty":"Medium","duration":"14 min read","keywords":["gitlab ci docker","gitlab ci build docker image","gitlab ci push docker image","docker in docker gitlab","gitlab ci kaniko","gitlab ci container registry","gitlab ci dockerfile","gitlab ci docker build push","dind gitlab"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":11,"tableOfContents":[{"id":"using-docker-images-in-ci-jobs-with-image","title":"Using Docker Images in CI Jobs with image","level":2},{"id":"why-this-matters","title":"Why this matters","level":3},{"id":"global-default-image-vs-per-job-image","title":"Global Default Image vs Per-Job Image","level":2},{"id":"global-default-image","title":"Global default image","level":3},{"id":"per-job-image","title":"Per-job image","level":3},{"id":"when-to-choose-each-approach","title":"When to choose each approach","level":3},{"id":"pulling-images-from-docker-hub-gitlab-container-registry-and-private-registries","title":"Pulling Images from Docker Hub, GitLab Container Registry, and Private Registries","level":2},{"id":"docker-hub","title":"Docker Hub","level":3},{"id":"gitlab-container-registry","title":"GitLab Container Registry","level":3},{"id":"private-registries","title":"Private registries","level":3},{"id":"building-a-docker-image-in-ci-the-real-challenge","title":"Building a Docker Image in CI: The Real Challenge","level":2},{"id":"docker-in-docker-dind-how-it-works","title":"Docker-in-Docker (DinD): How It Works","level":2},{"id":"what-is-happening-here","title":"What is happening here?","level":3},{"id":"why-dind-is-popular","title":"Why DinD Is Popular","level":2},{"id":"security-concerns-with-docker-in-docker","title":"Security Concerns with Docker-in-Docker","level":2},{"id":"when-to-avoid-dind","title":"When to avoid DinD","level":3},{"id":"kaniko-as-a-dind-alternative","title":"Kaniko as a DinD Alternative","level":2},{"id":"why-teams-like-kaniko","title":"Why teams like Kaniko","level":3},{"id":"tradeoffs","title":"Tradeoffs","level":3},{"id":"pushing-to-gitlab-container-registry","title":"Pushing to GitLab Container Registry","level":2},{"id":"docker-login-example","title":"Docker login example","level":3},{"id":"why-the-gitlab-registry-is-convenient","title":"Why the GitLab registry is convenient","level":3},{"id":"pushing-to-docker-hub","title":"Pushing to Docker Hub","level":2},{"id":"tagging-strategies-for-docker-images-in-gitlab-ci","title":"Tagging Strategies for Docker Images in GitLab CI","level":2},{"id":"commit-sha-tag","title":"Commit SHA tag","level":3},{"id":"git-tag-release","title":"Git tag release","level":3},{"id":"latest-tag","title":"Latest tag","level":3},{"id":"recommended-strategy","title":"Recommended strategy","level":3},{"id":"multi-stage-builds-and---target","title":"Multi-Stage Builds and --target","level":2},{"id":"complete-working-example-build-tag-and-push-to-gitlab-container-registry","title":"Complete Working Example: Build, Tag, and Push to GitLab Container Registry","level":2},{"id":"how-it-works","title":"How it works","level":3},{"id":"example-with-kaniko-instead-of-dind","title":"Example with Kaniko Instead of DinD","level":2},{"id":"common-mistakes-to-avoid","title":"Common Mistakes to Avoid","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I build a Docker image inside a GitLab CI job?","answer":"A CI job already runs inside a container, so there is no Docker daemon available by default and docker build fails with "Cannot connect to the Docker daemon". The two established approaches are Docker-in-Docker, where you attach a docker:dind service that provides a daemon for the job, and Kaniko, which builds images from a Dockerfile without needing a daemon at all. DinD is more familiar and requires privileged mode; Kaniko avoids that and is the safer option on shared runners."},{"question":"What is Docker-in-Docker and why is it a security concern?","answer":"DinD runs a Docker daemon as a service container alongside your job so the job can issue docker build and docker push. It requires the runner to be configured with privileged = true, and a privileged container can access the host's devices and kernel capabilities, which effectively means any job on that runner can take over the machine. On a runner dedicated to trusted code this may be acceptable; on a shared runner, or any project accepting merge requests from forks, it is a serious risk."},{"question":"What is Kaniko and when should I use it?","answer":"Kaniko builds container images from a Dockerfile inside a container, in userspace, with no Docker daemon and no privileged mode. It is the right default on shared or multi-tenant runners precisely because it removes the privilege escalation path that DinD opens. The trade-offs are that its caching behaviour differs from Docker's and a few exotic Dockerfile features are unsupported, so an existing DinD pipeline occasionally needs small adjustments when migrating."},{"question":"How do I push an image to the GitLab Container Registry?","answer":"GitLab provides predefined variables for this. The registry address, and a per-job token and username that authorise the push without you creating any credentials. Log in with those, tag the image using the registry image path variable, and push. Because the token is scoped to the job and expires with it, this is considerably safer than storing a long-lived registry password in CI/CD variables, and it needs no setup beyond enabling the registry on the project."},{"question":"How should I tag images built in CI?","answer":"Tag with something that traces back to exact source. The commit SHA is the standard choice, and GitLab supplies it as a predefined variable. Deploy that immutable tag rather than a moving one, so a rollback has a specific image to return to and an incident timeline can be reconstructed. It is common to additionally push a moving tag such as the branch name or latest for convenience, but nothing should be deployed by those, since two deploys of the same moving tag can run different code."},{"question":"Why does my job fail with "Cannot connect to the Docker daemon"?","answer":"Because nothing is providing one. Either add the docker:dind service and the environment variables that point the client at it, or switch to Kaniko, which does not need a daemon. A subtler variant of this error occurs when DinD is configured but the runner is not in privileged mode. The service container fails to start, and the job's error looks identical even though the cause is the runner configuration rather than the pipeline."}]},{"slug":"gitlab-ci-testing","title":"GitLab CI Testing","seoTitle":"GitLab CI Testing: JUnit Reports, Coverage and Jest Pipeline","description":"Run tests in GitLab CI: collect JUnit reports, view results in merge requests, extract coverage with a regex, run in parallel and follow a Jest pipeline.","content":"$3b","difficulty":"Easy","duration":"12 min read","keywords":["gitlab ci testing","gitlab ci unit tests","gitlab ci code coverage","gitlab ci test reports","gitlab ci junit report","gitlab ci coverage badge","gitlab ci pytest","gitlab ci jest","gitlab ci test artifacts"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":12,"tableOfContents":[{"id":"why-automated-testing-in-ci-is-non-negotiable","title":"Why Automated Testing in CI Is Non-Negotiable","level":2},{"id":"running-unit-tests-in-a-gitlab-ci-job","title":"Running Unit Tests in a GitLab CI Job","level":2},{"id":"nodejs-and-jest-example","title":"Node.js and Jest example","level":3},{"id":"python-and-pytest-example","title":"Python and pytest example","level":3},{"id":"collecting-junit-xml-reports","title":"Collecting JUnit XML Reports","level":2},{"id":"why-junit-reports-matter","title":"Why JUnit reports matter","level":3},{"id":"why-when-always-is-helpful","title":"Why when: always is helpful","level":3},{"id":"viewing-test-results-in-the-merge-request-ui","title":"Viewing Test Results in the Merge Request UI","level":2},{"id":"code-coverage-what-it-measures","title":"Code Coverage: What It Measures","level":2},{"id":"extracting-coverage-percentage-with-coverage-regex","title":"Extracting Coverage Percentage with coverage Regex","level":2},{"id":"why-regex-based-coverage-exists","title":"Why regex-based coverage exists","level":3},{"id":"coverage-visualization-in-merge-request-diffs","title":"Coverage Visualization in Merge Request Diffs","level":2},{"id":"coverage-badges-for-readme-files","title":"Coverage Badges for README Files","level":2},{"id":"failing-the-pipeline-on-coverage-drop","title":"Failing the Pipeline on Coverage Drop","level":2},{"id":"jest-example","title":"Jest example","level":3},{"id":"why-this-is-better-than-hoping","title":"Why this is better than hoping","level":3},{"id":"parallel-test-execution-with-parallel","title":"Parallel Test Execution with parallel","level":2},{"id":"test-environment-variables-in-ci","title":"Test Environment Variables in CI","level":2},{"id":"complete-example-nodejs-app-with-jest-coverage-and-junit-artifacts","title":"Complete Example: Node.js App with Jest, Coverage, and JUnit Artifacts","level":2},{"id":"what-this-pipeline-gives-you","title":"What this pipeline gives you","level":3},{"id":"example-python-with-pytest-and-junit","title":"Example: Python with pytest and JUnit","level":2},{"id":"common-mistakes-to-avoid","title":"Common Mistakes to Avoid","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I show test results in the GitLab merge request?","answer":"Have your test runner emit JUnit XML and declare it under artifacts: reports: junit:. GitLab parses the file and renders a test summary in the merge request showing which tests newly failed, which were fixed, and how long they took. Far more useful than reading a job log. Nearly every test framework can produce JUnit XML, usually through a reporter package or a command-line flag. Set artifacts: when: always so reports are still collected when the job fails, which is exactly when you need them."},{"question":"How do I get a code coverage percentage into GitLab?","answer":"Set the coverage: keyword on the job to a regular expression that matches the coverage number in the job's output. GitLab extracts it and displays it on the pipeline, on the merge request, and in the coverage badge for your README. For per-line coverage highlighting in the merge request diff, additionally publish a Cobertura or equivalent report under artifacts: reports: coverage_report:, which is a separate mechanism from the percentage regex."},{"question":"Should I fail the pipeline when coverage drops?","answer":"A threshold is useful but an absolute one is a blunt instrument. Teams either set it low enough to be meaningless or spend time writing tests to satisfy a number rather than to catch bugs. A more workable approach is to fail on a significant decrease relative to the target branch, which catches genuinely untested new code without penalising a refactor that removes well-covered lines. Whatever you choose, make it visible in the merge request so the conversation happens at review time."},{"question":"How do I speed up a slow test suite in CI?","answer":"Use parallel: to split the job into several instances that run simultaneously, with your test runner sharding by the index variables GitLab provides. Cache dependencies so installation is not repeated on every run. Split fast unit tests from slow integration tests into separate stages so the quick feedback arrives first and the pipeline fails early. If a suite is slow because of genuinely slow tests rather than volume, parallelism only hides the cost. The individual tests still need attention."},{"question":"Why do my tests pass locally but fail in CI?","answer":"Almost always environment differences. CI starts from a clean container with no leftover state, so tests that depend on a previously seeded database, a file written by an earlier run, or a locally installed tool will fail. Tests that assume a particular execution order also break under parallelism. Timezone and locale differ from a developer laptop surprisingly often. Reproduce it by running the same image locally with the same command the job uses, which usually surfaces the difference within minutes."},{"question":"Should CI run tests against a real database?","answer":"For integration tests, yes, and GitLab makes it straightforward with service containers. Declare Postgres or Redis under services: and it runs alongside the job, reachable over the network with a clean state per job. That is considerably more trustworthy than mocking the database and finding out about a query problem in production. Keep unit tests free of external dependencies so they stay fast, and reserve service containers for the tests that genuinely need them."}]},{"slug":"gitlab-ci-security","title":"GitLab CI Security Scanning","seoTitle":"GitLab CI Security Scanning: SAST, DAST, Secret Detection","description":"Add security scanning to GitLab CI: SAST, DAST, secret detection, dependency and container scanning, and DevSecOps habits for secure pipelines.","content":"$3c","difficulty":"Medium","duration":"12 min read","keywords":["gitlab ci security scanning","gitlab sast","gitlab dast","gitlab container scanning","gitlab dependency scanning","gitlab security dashboard","gitlab secret detection","devsecops gitlab","gitlab security reports"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":13,"tableOfContents":[{"id":"what-devsecops-means-in-practice","title":"What DevSecOps Means in Practice","level":2},{"id":"gitlab-built-in-security-scanning-overview","title":"GitLab Built-In Security Scanning Overview","level":2},{"id":"sast-static-application-security-testing","title":"SAST: Static Application Security Testing","level":2},{"id":"what-sast-scans","title":"What SAST scans","level":3},{"id":"how-to-enable-sast","title":"How to enable SAST","level":3},{"id":"sast-best-practices","title":"SAST best practices","level":3},{"id":"secret-detection","title":"Secret Detection","level":2},{"id":"what-secret-detection-catches","title":"What secret detection catches","level":3},{"id":"how-to-enable-secret-detection","title":"How to enable secret detection","level":3},{"id":"why-this-matters","title":"Why this matters","level":3},{"id":"dependency-scanning","title":"Dependency Scanning","level":2},{"id":"what-dependency-scanning-inspects","title":"What dependency scanning inspects","level":3},{"id":"how-to-enable-dependency-scanning","title":"How to enable dependency scanning","level":3},{"id":"why-dependency-scanning-matters","title":"Why dependency scanning matters","level":3},{"id":"container-scanning","title":"Container Scanning","level":2},{"id":"what-container-scanning-checks","title":"What container scanning checks","level":3},{"id":"how-to-enable-container-scanning","title":"How to enable container scanning","level":3},{"id":"dast-dynamic-application-security-testing","title":"DAST: Dynamic Application Security Testing","level":2},{"id":"what-dast-needs","title":"What DAST needs","level":3},{"id":"how-to-enable-dast","title":"How to enable DAST","level":3},{"id":"when-to-use-dast","title":"When to use DAST","level":3},{"id":"enabling-multiple-gitlab-security-scanners-together","title":"Enabling Multiple GitLab Security Scanners Together","level":2},{"id":"reading-security-reports-in-the-merge-request-ui","title":"Reading Security Reports in the Merge Request UI","level":2},{"id":"security-dashboard-and-vulnerability-management","title":"Security Dashboard and Vulnerability Management","level":2},{"id":"failing-the-pipeline-on-severity-thresholds","title":"Failing the Pipeline on Severity Thresholds","level":2},{"id":"practical-example-nodejs-project-with-sast-and-secret-detection","title":"Practical Example: Node.js Project with SAST and Secret Detection","level":2},{"id":"tips-for-making-gitlab-security-scanning-useful","title":"Tips for Making GitLab Security Scanning Useful","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What security scanners does GitLab include?","answer":"SAST analyses your source code for vulnerable patterns, Secret Detection looks for credentials committed to the repository, Dependency Scanning checks your declared dependencies against known CVEs, Container Scanning inspects built images for vulnerable operating system packages, and DAST exercises a running application for runtime issues. They are enabled by including GitLab's templates in your pipeline. Availability of the merge request widget, the security dashboard and vulnerability management varies by GitLab tier, though the scanners themselves run on all of them."},{"question":"Which scanner should I enable first?","answer":"Secret Detection, because a leaked credential is an immediate and unambiguous problem, and because it finds things regardless of language or framework. Dependency Scanning is a close second, since most vulnerabilities in a typical application arrive through its dependencies rather than its own code. SAST is valuable but noisier, and is best introduced once the team has capacity to triage findings. Adding all five at once usually produces a volume of results nobody acts on."},{"question":"Should I fail the pipeline on security findings?","answer":"On critical and high severity findings in new code, yes. On everything, no. A policy of zero findings gets disabled within a month because it blocks unrelated work on pre-existing issues. A practical approach is to fail only on new critical and high vulnerabilities introduced by the change, report the rest without blocking, and work through the existing backlog separately. GitLab's merge request widget shows newly introduced findings specifically, which is what makes this distinction workable."},{"question":"What is the difference between SAST and DAST?","answer":"SAST reads your source code without running it, so it can point at an exact file and line and runs early and fast in the pipeline. DAST attacks a running instance of the application from the outside, so it finds issues that only appear at runtime. Configuration problems, authentication flaws, injection that depends on real request handling, but it needs a deployed environment and takes much longer. They find genuinely different classes of problem and complement each other rather than competing."},{"question":"Secret Detection found a credential in an old commit. Is removing it from the code enough?","answer":"No. Git history is permanent, so anyone with repository access can still read the secret from the earlier commit, and any clone already contains it. Treat it as compromised: rotate the credential immediately, then remove it from the current code. Rewriting history to purge it is possible but disruptive and does not help if the repository was ever public or cloned. Rotation is the step that actually restores security; history cleanup is housekeeping."},{"question":"How do I stop security scanning slowing every pipeline down?","answer":"Run the heavier scanners where they earn their keep rather than on every commit. Secret Detection and Dependency Scanning are fast enough to run everywhere. Container Scanning naturally belongs after the image is built, and DAST needs a deployed environment so it fits on merge requests to the default branch or on a nightly schedule. Using rules to target them appropriately keeps the fast feedback loop fast while still getting the coverage."}]},{"slug":"gitlab-ci-deploy-aws","title":"GitLab CI Deploy to AWS","seoTitle":"GitLab CI Deploy to AWS: OIDC, S3, EC2, ECR and ECS","description":"Deploy to AWS from GitLab CI using OIDC instead of static keys, then ship to S3 and EC2, push images to ECR, update ECS services and automate Terraform runs.","content":"$3d","difficulty":"Medium","duration":"14 min read","keywords":["gitlab ci deploy aws","gitlab ci aws deployment","gitlab ci aws credentials","gitlab ci s3 deploy","gitlab ci ec2 deploy","gitlab ci ecs deploy","gitlab ci aws ecr","gitlab ci aws access key","gitlab ci terraform aws"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":14,"tableOfContents":[{"id":"overview-of-deploying-to-aws-from-gitlab-ci","title":"Overview of Deploying to AWS from GitLab CI","level":2},{"id":"aws-credential-management-access-keys-vs-oidc","title":"AWS Credential Management: Access Keys vs OIDC","level":2},{"id":"option-1-long-lived-access-keys","title":"Option 1: Long-lived access keys","level":3},{"id":"option-2-oidc-with-short-lived-credentials","title":"Option 2: OIDC with short-lived credentials","level":3},{"id":"using-aws-access-keys-in-gitlab-cicd-variables","title":"Using AWS Access Keys in GitLab CI/CD Variables","level":2},{"id":"using-oidc-to-avoid-long-lived-credentials","title":"Using OIDC to Avoid Long-Lived Credentials","level":2},{"id":"deploying-a-static-site-to-s3","title":"Deploying a Static Site to S3","level":2},{"id":"deploying-to-ec2-with-ssh","title":"Deploying to EC2 with SSH","level":2},{"id":"building-and-pushing-to-amazon-ecr","title":"Building and Pushing to Amazon ECR","level":2},{"id":"deploying-to-ecs","title":"Deploying to ECS","level":2},{"id":"running-terraform-from-gitlab-ci-for-aws-infrastructure","title":"Running Terraform from GitLab CI for AWS Infrastructure","level":2},{"id":"environment-specific-aws-deployments","title":"Environment-Specific AWS Deployments","level":2},{"id":"end-to-end-example-build-docker-image-push-to-ecr-deploy-to-ecs","title":"End-to-End Example: Build Docker Image, Push to ECR, Deploy to ECS","level":2},{"id":"practical-guidance-and-common-pitfalls","title":"Practical Guidance and Common Pitfalls","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Should I use AWS access keys or OIDC in GitLab CI?","answer":"OIDC, wherever it is available. With OIDC, GitLab issues a short-lived signed token that AWS exchanges for temporary credentials through an IAM role, so there is no long-lived secret stored anywhere. Nothing to leak, nothing to rotate, and the trust policy can be scoped to a specific project and branch. Access keys are simpler to set up and remain the fallback for older instances, but they are long-lived credentials sitting in CI/CD variables, which makes masking, protection and regular rotation essential."},{"question":"How do I set up OIDC between GitLab and AWS?","answer":"Create an IAM OIDC identity provider in your AWS account pointing at your GitLab instance, then create an IAM role whose trust policy accepts tokens from that provider with conditions restricting which project, and ideally which branch or environment, may assume it. In the pipeline, request an ID token and exchange it with aws sts assume-role-with-web-identity. The conditions in the trust policy are the important part, without scoping to a specific project path, any project on the instance could assume the role."},{"question":"What IAM permissions should the deploy role have?","answer":"Only the actions that specific deployment needs, scoped to specific resource ARNs. A static site deploy needs S3 object write on one bucket and a CloudFront invalidation on one distribution; an ECS deploy needs to register a task definition and update one service. Starting from a broad managed policy with the intention of narrowing later almost never gets narrowed. Separate roles per environment are worth the extra setup, so a staging pipeline cannot touch production."},{"question":"How do I deploy a static site to S3 from GitLab CI?","answer":"Sync the built files to the bucket with the AWS CLI, then invalidate the CloudFront distribution so viewers get the new version rather than a cached one. Two details matter: sync with --delete so removed files actually disappear rather than lingering, and set cache headers deliberately, long-lived caching for fingerprinted assets, short or no caching for index.html. Skipping the invalidation is the usual reason a deploy appears to succeed while users keep seeing the old site."},{"question":"Why does my AWS command fail with an access denied error in CI but work locally?","answer":"Because it is running as a different identity. Your laptop uses your personal credentials, which are typically far broader than the deploy role. Run aws sts get-caller-identity as the first line of the job to see exactly which principal the pipeline is using, then compare that against the policy. The other frequent cause is region: the CLI in CI has no configured default, so a command targeting a resource in one region while the CLI defaults to another fails in a way that reads like a permissions problem."},{"question":"Should I run Terraform from GitLab CI?","answer":"Yes, and it is a natural fit, but use a remote backend with state locking, typically S3 with DynamoDB or GitLab's own managed Terraform state, because local state in a job is discarded when the container exits. Run terraform plan on merge requests so the change is reviewable, and gate terraform apply behind a manual job on the default branch. Never let apply run automatically on a feature branch against shared infrastructure."}]},{"slug":"gitlab-ci-multi-project","title":"GitLab CI Multi-Project Pipelines","seoTitle":"GitLab Multi-Project and Child Pipelines: trigger Keyword","description":"Trigger downstream pipelines in GitLab with the trigger keyword: pass variables, choose strategy depend or mirror, build child pipelines and use include.","content":"$3e","difficulty":"Medium","duration":"10 min read","keywords":["gitlab multi-project pipelines","gitlab downstream pipeline","gitlab cross-project trigger","gitlab ci trigger keyword","gitlab pipeline trigger api","gitlab child pipeline","gitlab include remote","gitlab ci parent child pipeline"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":15,"tableOfContents":[{"id":"what-are-multi-project-pipelines-and-why-use-them","title":"What Are Multi-Project Pipelines and Why Use Them?","level":2},{"id":"triggering-a-downstream-pipeline-with-the-trigger-keyword","title":"Triggering a Downstream Pipeline with the trigger Keyword","level":2},{"id":"passing-variables-to-downstream-pipelines","title":"Passing Variables to Downstream Pipelines","level":2},{"id":"strategy-depend-vs-strategy-mirror","title":"strategy: depend vs strategy: mirror","level":2},{"id":"strategy-depend","title":"strategy: depend","level":3},{"id":"strategy-mirror","title":"strategy: mirror","level":3},{"id":"child-pipelines-with-include","title":"Child Pipelines with include","level":2},{"id":"the-include-keyword-local-remote-template-project","title":"The include Keyword: Local, Remote, Template, Project","level":2},{"id":"local","title":"local","level":3},{"id":"remote","title":"remote","level":3},{"id":"template","title":"template","level":3},{"id":"project","title":"project","level":3},{"id":"dynamic-child-pipelines","title":"Dynamic Child Pipelines","level":2},{"id":"cross-project-pipeline-dependencies-with-needsproject","title":"Cross-Project Pipeline Dependencies with needs:project","level":2},{"id":"triggering-pipelines-with-the-trigger-api","title":"Triggering Pipelines with the Trigger API","level":2},{"id":"when-to-use-multi-project-pipelines-vs-monorepo-pipelines","title":"When to Use Multi-Project Pipelines vs Monorepo Pipelines","level":2},{"id":"choose-multi-project-pipelines-when","title":"Choose multi-project pipelines when:","level":3},{"id":"choose-monorepo-or-child-pipelines-when","title":"Choose monorepo or child pipelines when:","level":3},{"id":"complete-example-frontend-backend-and-a-deploy-trigger","title":"Complete Example: Frontend, Backend, and a Deploy Trigger","level":2},{"id":"frontend-pipeline","title":"Frontend pipeline","level":3},{"id":"backend-pipeline","title":"Backend pipeline","level":3},{"id":"deployments-pipeline","title":"Deployments pipeline","level":3},{"id":"best-practices-for-multi-project-and-child-pipelines","title":"Best Practices for Multi-Project and Child Pipelines","level":2},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between a child pipeline and a multi-project pipeline?","answer":"A child pipeline is generated from a separate configuration file within the same project, using trigger: with an include:. A multi-project pipeline triggers a pipeline in a different project entirely, using trigger: with a project: key. Child pipelines are for splitting a large or monorepo configuration into manageable, independently-running pieces; multi-project pipelines are for coordinating across repositories, such as an application build that triggers a deployment project."},{"question":"What does strategy: depend do?","answer":"By default, a triggering job succeeds as soon as the downstream pipeline has been created, without waiting to see whether it passes. strategy: depend changes that so the upstream job mirrors the downstream result. It waits for completion and fails if the downstream pipeline fails. Without it, an upstream pipeline can report green while the deployment it triggered failed, which is a genuinely misleading state and the most common surprise with this feature."},{"question":"How do I share pipeline configuration between projects?","answer":"The include: keyword pulls in configuration from a local file, a remote URL, another project on the same instance, or one of GitLab's built-in templates. Including from a project is the usual choice for an internal template library, and you can pin a ref so consumers are not broken by an unreviewed change to the shared file. Combine it with YAML anchors or extends: to define a job template once and specialise it per project."},{"question":"What are dynamic child pipelines?","answer":"A job generates a .gitlab-ci.yml file as an artifact, and a second job triggers a child pipeline from it. Because the configuration is produced at run time, it can reflect what actually changed. A monorepo pipeline that inspects the diff and emits jobs only for affected services, for instance. It is a powerful pattern for large repositories where a static configuration would either run far too much or need constant hand-editing, at the cost of the pipeline being harder to read since it does not exist until it runs."},{"question":"How do I pass variables and artifacts between projects?","answer":"Variables declared under the triggering job's variables: are passed to the downstream pipeline automatically. Artifacts are not. A downstream job that needs a file from upstream fetches it with needs:project:, naming the upstream project, job and ref, which works across projects using the CI job token for authorisation. Make sure the downstream project's job token access settings permit the upstream project, since tightened defaults are a common reason cross-project fetches start failing."},{"question":"When should I split into multiple projects rather than one pipeline?","answer":"Split when the components have genuinely independent release cycles, different ownership, or different access requirements. A shared library consumed by several teams, or a deployment project that holds production credentials the application repositories should not have. Keep a monorepo pipeline when components are released together, since coordinating versions across repositories is real ongoing work. For a monorepo that has simply grown unwieldy, child pipelines usually solve the problem without splitting the repository at all."}]},{"slug":"gitlab-ci-best-practices","title":"GitLab CI Best Practices","seoTitle":"GitLab CI Best Practices: Faster, Cleaner Pipelines","description":"Speed up GitLab CI pipelines with better caching, DAG execution using needs, reusable include templates, YAML anchors, rules and protected deployments.","content":"$3f","difficulty":"Medium","duration":"12 min read","keywords":["gitlab ci best practices","gitlab pipeline optimization","gitlab ci performance","gitlab pipeline speed","gitlab ci templates","gitlab ci include","gitlab ci yaml anchors","gitlab ci tips","gitlab ci efficient pipelines","gitlab ci job dependencies"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":16,"tableOfContents":[{"id":"principle-1-keep-pipelines-fast","title":"Principle 1: Keep Pipelines Fast","level":2},{"id":"principle-2-fail-fast","title":"Principle 2: Fail Fast","level":2},{"id":"principle-3-use-caching-effectively","title":"Principle 3: Use Caching Effectively","level":2},{"id":"nodejs-caching","title":"Node.js caching","level":3},{"id":"python-caching","title":"Python caching","level":3},{"id":"maven-caching","title":"Maven caching","level":3},{"id":"principle-4-use-artifacts-sparingly","title":"Principle 4: Use Artifacts Sparingly","level":2},{"id":"principle-5-dry-pipelines-with-yaml-anchors-and-aliases","title":"Principle 5: DRY Pipelines with YAML Anchors and Aliases","level":2},{"id":"principle-6-use-include-to-share-pipeline-templates","title":"Principle 6: Use include to Share Pipeline Templates","level":2},{"id":"principle-7-keep-secrets-out-of-the-pipeline-definition","title":"Principle 7: Keep Secrets Out of the Pipeline Definition","level":2},{"id":"principle-8-use-needs-for-dag-pipelines","title":"Principle 8: Use needs for DAG Pipelines","level":2},{"id":"principle-9-use-rules-instead-of-only-onlyexcept","title":"Principle 9: Use rules Instead of Only only/except","level":2},{"id":"principle-10-protect-production-with-manual-gates-and-protected-environments","title":"Principle 10: Protect Production with Manual Gates and Protected Environments","level":2},{"id":"common-mistakes-to-avoid","title":"Common Mistakes to Avoid","level":2},{"id":"no-caching","title":"No caching","level":3},{"id":"using-latest-images-without-pinning","title":"Using latest images without pinning","level":3},{"id":"too-many-stages","title":"Too many stages","level":3},{"id":"artifact-sprawl","title":"Artifact sprawl","level":3},{"id":"copy-paste-yaml-everywhere","title":"Copy-paste YAML everywhere","level":3},{"id":"hidden-secrets-in-logs","title":"Hidden secrets in logs","level":3},{"id":"measuring-pipeline-performance-with-gitlab-ci-analytics","title":"Measuring Pipeline Performance with GitLab CI Analytics","level":2},{"id":"refactored-pipeline-example-before-vs-after","title":"Refactored Pipeline Example: Before vs After","level":2},{"id":"before","title":"Before","level":3},{"id":"after","title":"After","level":3},{"id":"final-takeaway","title":"Final Takeaway","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I make my GitLab pipeline faster?","answer":"In rough order of payoff: cache dependencies so installation is not repeated every run; use needs: to turn a strictly staged pipeline into a DAG so jobs start as soon as their own dependencies are ready; run independent jobs in parallel and split large test suites with parallel:; put fast checks such as linting first so obvious failures stop the pipeline early; and use rules:changes so untouched parts of a monorepo are skipped. Measure with the pipeline analytics before and after. The slowest job is rarely the one people assume."},{"question":"What is the difference between YAML anchors and extends?","answer":"Anchors are a YAML feature that copies a block wherever it is referenced, and they only work within a single file. extends: is a GitLab keyword that merges a job template into another job, works across included files, and merges nested keys intelligently rather than replacing them wholesale. extends: is the better choice for anything shared, particularly once configuration is split with include:, because anchors cannot cross file boundaries."},{"question":"How should I structure a large .gitlab-ci.yml?","answer":"Split it. Keep the root file as a short index of include: statements plus the stages: and workflow: definitions, and move job definitions into files grouped by concern. Build, test, deploy, security. Define shared job templates once and specialise them with extends:. For templates used by several projects, move them into a dedicated repository and include them from there with a pinned ref so a change to the shared file cannot break every consumer at once."},{"question":"What are the most common mistakes in GitLab CI pipelines?","answer":"Hardcoding secrets in .gitlab-ci.yml instead of using masked and protected variables; depending on cache for correctness rather than only for speed; keeping every artifact indefinitely until the storage quota is hit; using only/except in new pipelines rather than rules; leaving every job in a strict stage sequence when needs: would let independent work run concurrently; and deploying moving tags such as latest so a rollback has no specific artifact to return to."},{"question":"Should every job run on every commit?","answer":"No, and a pipeline that does is usually slower than it needs to be. Use rules so expensive jobs run where they add value. A full integration suite on merge requests to the default branch and on a schedule, rather than on every push to every feature branch. rules:changes skips jobs when the relevant files were not touched, which matters enormously in a monorepo. The balance to aim for is that every push gets fast, meaningful feedback, and the exhaustive checks run before merge."},{"question":"How do I protect production deployments?","answer":"Three mechanisms together. Mark the production credentials as protected variables so only protected branches and tags can read them. Use a protected environment, which lets GitLab enforce who may deploy and can require approvals. And set when: manual on the deploy job so a person deliberately triggers it. Each covers a different gap. The variable stops an untrusted branch reading secrets, the environment stops an unauthorised person deploying, and the manual gate stops an accidental deploy."}]}],"category":"cicd","errorPages":[{"slug":"gitlab-job-stuck-no-active-runners","title":"This job is stuck because you don't have any active runners","description":"Fix GitLab "job is stuck, no active runners": match job tags to the runner, allow protected branches, and check the runner is online. Covers shared runners."},{"slug":"gitlab-could-not-read-username","title":"fatal: could not read Username for 'https://gitlab.com': No such device or address","description":"Fix "could not read Username for gitlab.com" in CI: set git config insteadOf with CI_JOB_TOKEN, allow the job token, or use a deploy key over SSH."},{"slug":"github-actions-resource-not-accessible","title":"Error: Resource not accessible by integration","description":"Fix "Resource not accessible by integration": add the missing scope to the permissions block, such as contents: write or pull-requests: write. Covers forks."},{"slug":"ci-oidc-could-not-assume-role","title":"Not authorized to perform sts:AssumeRoleWithWebIdentity","description":"Fix "Not authorized to perform sts:AssumeRoleWithWebIdentity": add id-token: write, print the OIDC claims, and match the trust policy sub and aud exactly."}],"children":"$L40"}]
40:["$","div",null,{"className":"max-w-none","children":"$L41"}]
41:[["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["CI/CD is one of the most important ideas in modern software delivery. ",["$","strong",null,{"children":"CI/CD stands for Continuous Integration and Continuous Delivery (or Continuous Deployment): the practice of automatically building, testing, and releasing code every time a developer makes a change, so software can go from a commit to production quickly and reliably."}]," If you have ever wondered why some teams release several times a day while others are terrified to deploy once a month, the answer often comes down to whether they have built a strong ",["$","strong",null,{"children":"CI/CD pipeline"}],". In simple terms, CI/CD is the practice of using automation to move code from a developer's commit to a tested, deployable, and sometimes production-ready release."]}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["This tutorial is an in-depth ",["$","strong",null,{"children":"introduction to CI/CD for beginners"}],". By the end, you will understand what CI/CD means, why teams needed it in the first place, how ",["$","strong",null,{"children":"Continuous Integration"}],", ",["$","strong",null,{"children":"Continuous Delivery"}],", and ",["$","strong",null,{"children":"Continuous Deployment"}]," differ, what a pipeline is made of, and why ",["$","strong",null,{"children":["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/cicd/gitlab-cicd-getting-started","children":"GitLab CI/CD"}]}]," is such a practical place to start."]}],"\n",["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"the-problem-before-cicd","children":"The Problem Before CI/CD"}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"To understand why CI/CD matters, it helps to imagine software development before these practices became common."}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A team of developers would each work on features for days or weeks in isolation. One person changed the login page. Another updated the database schema. A third modified the API. A fourth added a background job. Everyone was productive on their own machine, but the real pain started when it was time to combine everything."}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["This painful moment was often called ",["$","strong",null,{"children":"integration hell"}],"."]}],"\n",["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"what-integration-hell-looked-like","children":"What integration hell looked like"}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Integration hell happened when teams delayed merging their changes for too long. Because everyone was working separately, changes drifted apart. One branch depended on an old API contract. Another assumed a package version that had changed. A third introduced a configuration difference that only appeared in staging. When the team finally merged everything together, hidden conflicts exploded at once."}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Common symptoms included:"}],"\n","$L42","\n","$L43","\n","$L44","\n","$L45","\n","$L46","\n","$L47","\n","$L48","\n","$L49","\n","$L4a","\n","$L4b","\n","$L4c","\n","$L4d","\n","$L4e","\n","$L4f","\n","$L50","\n","$L51","\n","$L52","\n","$L53","\n","$L54","\n","$L55","\n","$L56","\n","$L57","\n","$L58","\n","$L59","\n","$L5a","\n","$L5b","\n","$L5c","\n","$L5d","\n","$L5e","\n","$L5f","\n","$L60","\n","$L61","\n","$L62","\n","$L63","\n","$L64","\n","$L65","\n","$L66","\n","$L67","\n","$L68","\n","$L69","\n","$L6a","\n","$L6b","\n","$L6c","\n","$L6d","\n","$L6e","\n","$L6f","\n","$L70","\n","$L71","\n","$L72","\n","$L73","\n","$L74","\n","$L75","\n","$L76","\n","$L77","\n","$L78","\n","$L79","\n","$L7a","\n","$L7b","\n","$L7c","\n","$L7d","\n","$L7e","\n","$L7f","\n","$L80","\n","$L81","\n","$L82","\n","$L83","\n","$L84","\n","$L85","\n","$L86","\n","$L87","\n","$L88","\n","$L89","\n","$L8a","\n","$L8b","\n","$L8c","\n","$L8d","\n","$L8e","\n","$L8f","\n","$L90","\n","$L91","\n","$L92","\n","$L93","\n","$L94","\n","$L95","\n","$L96","\n","$L97","\n","$L98","\n","$L99","\n","$L9a","\n","$L9b","\n","$L9c","\n","$L9d","\n","$L9e","\n","$L9f","\n","$La0","\n","$La1","\n","$La2","\n","$La3","\n","$La4","\n","$La5","\n","$La6","\n","$La7","\n","$La8","\n","$La9","\n","$Laa","\n","$Lab","\n","$Lac","\n","$Lad","\n","$Lae","\n","$Laf","\n","$Lb0","\n","$Lb1","\n","$Lb2","\n","$Lb3","\n","$Lb4","\n","$Lb5","\n","$Lb6","\n","$Lb7","\n","$Lb8","\n","$Lb9","\n","$Lba","\n","$Lbb","\n","$Lbc","\n","$Lbd","\n","$Lbe","\n","$Lbf","\n","$Lc0","\n","$Lc1","\n","$Lc2","\n","$Lc3","\n","$Lc4","\n","$Lc5","\n","$Lc6","\n","$Lc7","\n","$Lc8","\n","$Lc9","\n","$Lca","\n","$Lcb","\n","$Lcc","\n","$Lcd","\n","$Lce","\n","$Lcf","\n","$Ld0","\n","$Ld1","\n","$Ld2","\n","$Ld3","\n","$Ld4","\n","$Ld5","\n","$Ld6","\n","$Ld7","\n","$Ld8","\n","$Ld9","\n","$Lda","\n","$Ldb","\n","$Ldc","\n","$Ldd","\n","$Lde","\n","$Ldf","\n","$Le0","\n","$Le1","\n","$Le2","\n","$Le3","\n","$Le4","\n","$Le5","\n","$Le6","\n","$Le7","\n","$Le8","\n","$Le9","\n","$Lea","\n","$Leb","\n","$Lec","\n","$Led","\n","$Lee","\n","$Lef","\n","$Lf0","\n","$Lf1","\n","$Lf2","\n","$Lf3","\n","$Lf4","\n","$Lf5","\n","$Lf6"]
fb:I[18308,["8500","static/chunks/8500-98e13bcce54aa7a0.js","3839","static/chunks/3839-ab8b319d7c7091ea.js","5899","static/chunks/5899-9fb98f224ca30c06.js","9588","static/chunks/app/tutorials/cicd/%5Bslug%5D/page-d87ebbccc858a0bc.js"],"default"]
42:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"builds that suddenly stopped compiling"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"tests that failed only after several branches were combined"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"deployment scripts that worked for one developer but not for another"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"bugs that appeared late, when release deadlines were already close"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"long debugging sessions because nobody knew which change caused the breakage"}],"\n"]}]
43:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Instead of a smooth flow, software delivery became a stressful event."}]
44:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"manual-deployments-made-everything-worse","children":"Manual deployments made everything worse"}]
45:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Even when the code technically worked, getting it into production was often a manual checklist. Someone would log into a server, pull the latest code, install dependencies, restart services, clear caches, and hope nothing was missed. Maybe a wiki page documented the process. Maybe it did not. Maybe only one experienced engineer knew the right sequence of commands."}]
46:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Manual deployments created several risks:"}]
47:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"steps were forgotten or done in the wrong order"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"production and staging drifted apart over time"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"deployments happened only during quiet hours because everyone expected trouble"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"rollback was unclear or impossible"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"audits were difficult because there was no reliable history of what ran, when, and why"}],"\n"]}]
48:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"late-stage-bugs-were-expensive","children":"Late-stage bugs were expensive"}]
49:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The later a bug is found, the more expensive it usually is to fix. If a unit test fails two minutes after a commit, the developer can correct it quickly. If the same bug is discovered three weeks later during release testing, the team must re-create the context, trace multiple changes, and delay the release."}]
4a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Before CI/CD, teams often found problems too late because they waited too long to integrate, test, and deliver. That delay slowed learning and increased risk."}]
4b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"CI/CD emerged as the answer to this exact set of problems."}]
4c:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-is-continuous-integration-ci","children":"What Is Continuous Integration (CI)?"}]
4d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Continuous Integration"}],", usually shortened to ",["$","strong",null,{"children":"CI"}],", is the practice of merging code into a shared repository frequently and automatically validating that code every time a change is made."]}]
4e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The key idea is simple: instead of integrating large batches of work at the end, developers integrate small changes continuously."}]
4f:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"what-happens-in-continuous-integration","children":"What happens in Continuous Integration"}]
50:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"In a CI workflow, developers commit code regularly. Each push or merge request triggers automation that might:"}]
51:["$","ol",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-decimal marker:text-teal-600 marker:font-semibold","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"fetch the latest code"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"install dependencies"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"compile or build the application"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"run unit tests"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"run linting or static checks"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"report the result back to the team"}],"\n"]}]
52:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"If something fails, the team knows immediately."}]
53:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That fast feedback loop changes everything. Problems become smaller, easier to understand, and much cheaper to fix."}]
54:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"why-frequent-merging-matters","children":"Why frequent merging matters"}]
55:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"CI encourages developers to merge code frequently rather than keeping large feature branches open for long periods. Small merges reduce the chance of conflicts and make failures easier to isolate. When a pipeline fails after a small change, you usually know where to look. When it fails after three weeks of accumulated work, debugging is much harder."}]
56:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"the-real-goal-of-ci","children":"The real goal of CI"}]
57:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The goal of CI is not just ârun tests automatically.â The deeper goal is to keep the codebase in a healthy, integrated state all the time."}]
58:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A strong CI setup gives a team confidence that:"}]
59:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"the current branch still builds"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"new changes did not break existing functionality"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"integration problems are caught early"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"code can move forward without large merge crises"}],"\n"]}]
5a:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-is-continuous-delivery-cd","children":"What Is Continuous Delivery (CD)?"}]
5b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["The second part of CI/CD is commonly ",["$","strong",null,{"children":"Continuous Delivery"}],"."]}]
5c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Continuous Delivery"}]," means that your code is always kept in a ",["$","strong",null,{"children":"deployable state"}],". After the build and tests pass, the software is ready to be released. A human may still choose when to deploy, but the hard technical work of preparing the release has already been automated."]}]
5d:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"the-push-button-release-idea","children":"The push-button release idea"}]
5e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Think of Continuous Delivery as âpush-button releases.â If the business decides it is the right time, the team should be able to deploy quickly and safely without scrambling to prepare the application."}]
5f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That means the pipeline has already handled key steps such as:"}]
60:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"building the application"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"validating it with automated tests"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["packaging it into a release artifact or ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/docker/docker-images","children":"container image"}]]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"preparing deployment steps for a target environment"}],"\n"]}]
61:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The software is ready. The final production deployment simply waits for approval."}]
62:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"why-teams-like-continuous-delivery","children":"Why teams like Continuous Delivery"}]
63:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Many organizations want automation but still keep a human gate before production. That approval may exist for business timing, compliance, change management, or risk control. Continuous Delivery supports that model well."}]
64:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"For example:"}]
65:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a bank may require release approval during business hours"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a SaaS team may wait until support coverage is online"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a product team may coordinate deployments with a marketing launch"}],"\n"]}]
66:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"In each case, the software remains ready to ship, but the final decision is manual."}]
67:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-is-continuous-deployment","children":"What Is Continuous Deployment?"}]
68:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Continuous Deployment"}]," goes one step further."]}]
69:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"In Continuous Deployment, every change that successfully passes the pipeline is automatically deployed to production with no manual approval step."}]
6a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That sounds bold, but it becomes realistic when a team has:"}]
6b:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"reliable automated tests"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"strong monitoring and alerting"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"safe deployment strategies"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"confidence in rollback or progressive delivery techniques"}],"\n"]}]
6c:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"delivery-vs-deployment","children":"Delivery vs Deployment"}]
6d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"This is the difference many beginners confuse:"}]
6e:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Delivery"}],": passing changes are ready for production, but a person decides when to deploy."]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Deployment"}],": passing changes are deployed to production automatically."]}],"\n"]}]
6f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Only the final gate changes, but that gate matters."}]
70:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"when-continuous-deployment-works-well","children":"When Continuous Deployment works well"}]
71:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Continuous Deployment is common in mature teams that release small changes very frequently. Because each release is small, risk is lower. Because deployment is automatic, the path to production stays fast and routine rather than rare and stressful."}]
72:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That does not mean every team should start there. Many teams begin with CI, move into Continuous Delivery, and adopt Continuous Deployment only after their testing and operational maturity improve."}]
73:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"ci-vs-cd-vs-continuous-deployment","children":"CI vs CD vs Continuous Deployment"}]
74:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The three ideas are closely related, but they solve different parts of the delivery problem."}]
75:["$","div",null,{"className":"overflow-x-auto mb-4 -mx-4 sm:mx-0","children":["$","div",null,{"className":"min-w-full px-4 sm:px-0","children":["$","table",null,{"className":"min-w-full divide-y divide-slate-300 border border-slate-300 rounded-lg text-sm","children":[["$","thead",null,{"className":"bg-slate-100","children":["$","tr",null,{"children":[["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Practice"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Main Goal"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"What Happens Automatically"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Human Involvement"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Example"}]]}]}],["$","tbody",null,{"children":[["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Continuous Integration"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Keep code integrated and validated"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Build, test, and feedback on every commit or merge request"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Developers fix failures quickly"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"A push triggers tests and reports success or failure"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Continuous Delivery"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Keep the application always deployable"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Build, test, package, and prepare release steps"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"A human approves production deployment"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"A passing pipeline creates a release candidate ready for one-click deploy"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Continuous Deployment"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Release every passing change automatically"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Build, test, package, and deploy to production"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"No approval before production"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["A commit to ","$Lf7"," passes all checks and goes live immediately"]}]]}]]}]]}]}]}]
76:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A simple way to remember it:"}]
77:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"CI"}]," answers: âDid this change integrate safely?â"]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Delivery"}]," answers: âCan we deploy this right now if we want to?â"]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Deployment"}]," answers: âShould the system deploy it automatically because it passed?â"]}],"\n"]}]
78:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"the-anatomy-of-a-cicd-pipeline","children":"The Anatomy of a CI/CD Pipeline"}]
79:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"CI/CD pipeline"}]," is the automated path that code follows after a trigger, such as a push, merge request, tag, or scheduled event."]}]
7a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The exact structure varies, but most beginner-friendly pipelines follow a familiar shape:"}]
7b:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-text text-sm","children":"commit -> build -> test -> deploy\n"}]}]
7c:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"stages","children":"Stages"}]
7d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"stage"}]," is a logical phase of the pipeline. Common stages include:"]}]
7e:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"build"}],": compile code, install dependencies, create packages, build ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/docker/introduction-to-docker","children":"Docker"}]," images"]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"test"}],": run unit tests, integration tests, security checks, linting"]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"deploy"}],": release to staging, production, or another target environment"]}],"\n"]}]
7f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Stages help organize work in the right order. Usually a later stage does not run unless earlier stages succeed."}]
80:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"jobs","children":"Jobs"}]
81:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"job"}]," is a specific unit of work inside a stage."]}]
82:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["For example, the ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"test"}]," stage might contain multiple jobs:"]}]
83:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"one job for unit tests"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"one job for linting"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"one job for security scanning"}],"\n"]}]
84:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Jobs are where commands actually run."}]
85:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"triggers","children":"Triggers"}]
86:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"trigger"}]," is the event that starts a pipeline. Common triggers include:"]}]
87:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"pushing to a branch"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"opening or updating a merge request"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["creating a ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/git/introduction-to-git","children":"Git"}]," tag"]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"clicking a manual run button"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a scheduled time-based execution"}],"\n"]}]
88:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Triggers let teams decide when automation should run."}]
89:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"key-cicd-concepts-you-should-know","children":"Key CI/CD Concepts You Should Know"}]
8a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["If you are learning ",["$","strong",null,{"children":"what a CI/CD pipeline is"}],", these terms appear everywhere."]}]
8b:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"pipeline","children":"Pipeline"}]
8c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"pipeline"}]," is the full automated workflow for a change. It contains stages and jobs and produces results like pass, fail, or blocked."]}]
8d:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"job","children":"Job"}]
8e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"job"}]," is one task performed by the pipeline, such as running tests or deploying an app."]}]
8f:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"stage","children":"Stage"}]
90:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"stage"}]," groups related jobs and helps define order, such as build before test and test before deploy."]}]
91:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"artifact","children":"Artifact"}]
92:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["An ",["$","strong",null,{"children":"artifact"}]," is a file produced by a job and passed along or saved for later use. Examples include:"]}]
93:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a compiled binary"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a test report"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"a zipped website build"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["a generated ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":".env"}]," file for downstream jobs"]}],"\n"]}]
94:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Artifacts help jobs share outputs safely."}]
95:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"runner","children":"Runner"}]
96:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"runner"}]," is the machine or agent that actually executes CI/CD jobs. In GitLab, a ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/cicd/gitlab-runners","children":"GitLab Runner"}]," picks up work and runs it using a chosen executor, often Docker."]}]
97:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"trigger","children":"Trigger"}]
98:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"trigger"}]," starts the pipeline. It is the âwhy now?â event behind the automation."]}]
99:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Understanding these terms makes documentation and pipeline files much easier to read."}]
9a:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"a-simple-mental-model-commit---pipeline---feedback-loop","children":"A Simple Mental Model: Commit -> Pipeline -> Feedback Loop"}]
9b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"One of the best beginner mental models is this:"}]
9c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"commit -> pipeline -> feedback loop"}]}]
9d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A developer makes a commit. That commit triggers a pipeline. The pipeline runs automated checks and gives fast feedback. If the change is healthy, it moves forward. If not, the team fixes it right away."}]
9e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That is the heart of CI/CD."}]
9f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Instead of waiting for a big release day to discover problems, teams shorten the loop between writing code and learning whether it works."}]
a0:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Short feedback loops create better engineering habits:"}]
a1:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"smaller changes"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"quicker fixes"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"more confidence"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"fewer surprises"}],"\n"]}]
a2:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"benefits-of-cicd","children":"Benefits of CI/CD"}]
a3:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"CI/CD is not just about automation for its own sake. It creates real operational and developer benefits."}]
a4:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"faster-release-cycles","children":"Faster release cycles"}]
a5:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"When the path from commit to release is automated, teams can ship more often. You do not need a long manual checklist for every deployment. Smaller, more frequent releases become practical."}]
a6:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"fewer-bugs-in-production","children":"Fewer bugs in production"}]
a7:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Because changes are tested continuously, broken code is caught earlier. Automated checks create a quality gate before code reaches users."}]
a8:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"higher-developer-confidence","children":"Higher developer confidence"}]
a9:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Developers work better when they trust the system around them. If every commit is validated automatically, engineers can refactor, improve, and release with much more confidence."}]
aa:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"better-consistency","children":"Better consistency"}]
ab:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Manual steps often vary depending on who runs them. A pipeline runs the same way every time, which reduces hidden differences between developers, environments, and deployments."}]
ac:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"clear-audit-trail","children":"Clear audit trail"}]
ad:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Modern CI/CD platforms record who triggered a pipeline, what commit was built, which jobs passed, and when deployment happened. That history is valuable for debugging, compliance, and incident review."}]
ae:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"easier-collaboration","children":"Easier collaboration"}]
af:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Because everyone works against the same automated standards, collaboration improves. New team members can understand the delivery process by reading the pipeline configuration instead of learning secret tribal knowledge."}]
b0:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"a-real-pipeline-narrative-without-code","children":"A Real Pipeline Narrative Without Code"}]
b1:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Imagine a small web application team using GitLab."}]
b2:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A developer fixes a bug in the checkout process and pushes the change to a branch. Immediately, a pipeline starts."}]
b3:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"First, the build stage installs dependencies and packages the app. Next, the test stage runs unit tests, a few API checks, and a linter. Everything passes. Then the deploy stage publishes the build to a staging environment where teammates can review it."}]
b4:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A product manager confirms the fix works. Because the team uses Continuous Delivery, production deployment is available as a manual approval step. An engineer clicks deploy, and the exact tested artifact goes live."}]
b5:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"In a more mature setup using Continuous Deployment, that last click would not be necessary. The same passing pipeline would send the change straight to production automatically."}]
b6:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Notice what did ",["$","strong",null,{"children":"not"}]," happen:"]}]
b7:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"nobody manually copied files to a server"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"nobody guessed which version was deployed"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"nobody skipped tests because of time pressure"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"nobody waited until the end of the month to discover an integration problem"}],"\n"]}]
b8:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That is the practical power of CI/CD."}]
b9:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"the-cicd-tools-landscape","children":"The CI/CD Tools Landscape"}]
ba:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Many tools can implement CI/CD. The best choice depends on your workflow, hosting model, team size, and platform preferences. Here is a brief beginner comparison of major tools."}]
bb:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"gitlab-cicd","children":"GitLab CI/CD"}]
bc:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["GitLab CI/CD is built directly into GitLab. Pipelines are defined in ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":".gitlab-ci.yml"}],", stored in the repository, and executed by runners. It is a strong all-in-one option for teams who want version control, merge requests, and CI/CD in one platform."]}]
bd:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"github-actions","children":"GitHub Actions"}]
be:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/github-actions/introduction-to-github-actions","children":"GitHub Actions"}]," is tightly integrated with GitHub. Workflows live in ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tools/yaml-validator","children":"YAML"}]," files under ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":".github/workflows"}],". It is popular for GitHub-centered teams and has a large marketplace of reusable actions."]}]
bf:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"jenkins","children":"Jenkins"}]
c0:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Jenkins is one of the oldest and most flexible CI/CD tools. It can integrate with almost anything, but it usually requires more setup, plugin management, and maintenance than newer built-in platforms."}]
c1:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"circleci","children":"CircleCI"}]
c2:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"CircleCI is a cloud-focused CI/CD tool known for speed and ease of use. It supports configuration as code and integrates with popular version control systems."}]
c3:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"argo-cd","children":"Argo CD"}]
c4:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Argo CD focuses on GitOps-style ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/kubernetes/kubernetes-deployments","children":"Kubernetes deployments"}]," rather than general-purpose CI. Teams often pair it with another CI tool. It is especially strong when Git is treated as the source of truth for cluster state."]}]
c5:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"quick-comparison","children":"Quick comparison"}]
c6:["$","div",null,{"className":"overflow-x-auto mb-4 -mx-4 sm:mx-0","children":["$","div",null,{"className":"min-w-full px-4 sm:px-0","children":["$","table",null,{"className":"min-w-full divide-y divide-slate-300 border border-slate-300 rounded-lg text-sm","children":[["$","thead",null,{"className":"bg-slate-100","children":["$","tr",null,{"children":[["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Tool"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Best Known For"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Strength"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Trade-off"}]]}]}],["$","tbody",null,{"children":[["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"GitLab CI/CD"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["All-in-one ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/what-is-devops","children":"DevOps"}]," workflow"]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Built-in experience and YAML in repo"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Best fit if you already use GitLab"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"GitHub Actions"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"GitHub-native automation"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Easy workflow sharing and ecosystem"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"GitHub-centric"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Jenkins"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Extreme flexibility"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Mature and highly customizable"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Higher maintenance burden"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"CircleCI"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Cloud CI convenience"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Fast setup and clean developer experience"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Separate platform to manage"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Argo CD"}],"$Lf8","$Lf9","$Lfa"]}]]}]]}]}]}]
c7:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"why-gitlab-cicd-is-a-great-choice","children":"Why GitLab CI/CD Is a Great Choice"}]
c8:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["If you are learning ",["$","strong",null,{"children":"GitLab CI/CD for beginners"}],", GitLab is a great place to start for several reasons."]}]
c9:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"built-in-not-bolted-on","children":"Built in, not bolted on"}]
ca:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"GitLab includes source control, merge requests, issues, and CI/CD in one platform. You do not need a separate CI server just to run your first pipeline."}]
cb:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"pipeline-as-code","children":"Pipeline as code"}]
cc:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["GitLab uses a ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":".gitlab-ci.yml"}]," file stored in the repository. That means your pipeline configuration is versioned, reviewable, and easy to evolve with the application itself."]}]
cd:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"no-separate-server-required-for-basic-use","children":"No separate server required for basic use"}]
ce:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"On GitLab.com, shared runners often let teams get started without provisioning their own infrastructure. That lowers the barrier to entry."}]
cf:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"strong-visibility","children":"Strong visibility"}]
d0:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"GitLab makes it easy to see pipeline status, job logs, artifacts, environments, and deployment history in one interface."}]
d1:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"scales-with-maturity","children":"Scales with maturity"}]
d2:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"You can begin with a tiny pipeline that only echoes a message, then grow into testing, caching, Docker builds, environments, deployments, security scans, and more."}]
d3:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"common-misunderstandings-beginners-have","children":"Common Misunderstandings Beginners Have"}]
d4:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["When learning ",["$","strong",null,{"children":"CI CD explained"}],", a few mistakes are very common."]}]
d5:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"cicd-is-only-for-big-companies","children":"âCI/CD is only for big companiesâ"}]
d6:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Not true. Small teams often benefit even more because automation removes repetitive manual work and reduces dependency on a single expert."}]
d7:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"cicd-means-automatic-production-deploys","children":"âCI/CD means automatic production deploysâ"}]
d8:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Not always. Continuous Deployment does, but many teams use CI plus Continuous Delivery, where production deployment still requires approval."}]
d9:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"a-pipeline-is-just-testing","children":"âA pipeline is just testingâ"}]
da:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Testing is a major part of CI/CD, but pipelines can also build artifacts, scan dependencies, publish packages, deploy environments, and generate reports."}]
db:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"we-can-add-cicd-later","children":"âWe can add CI/CD laterâ"}]
dc:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"You can, but delaying often means bad habits become normal first. Even a simple pipeline started early gives useful feedback and establishes a healthier workflow."}]
dd:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"bringing-it-all-together","children":"Bringing It All Together"}]
de:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A modern software team wants short feedback loops, reliable releases, and fewer deployment surprises. CI/CD supports all three."}]
df:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Integration"}]," helps teams merge often and validate changes automatically."]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Delivery"}]," keeps software ready to release at any time."]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Continuous Deployment"}]," automatically ships every passing change to production."]}],"\n"]}]
e0:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Together, these practices replace manual, error-prone release work with consistent automation. They reduce integration hell, catch bugs earlier, improve developer confidence, and create a clear delivery history."}]
e1:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"If you remember just one mental model from this introduction, make it this:"}]
e2:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"a commit triggers a pipeline, and the pipeline creates a feedback loop that helps code move safely toward production"}],"."]}]
e3:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That idea sits at the center of every good CI/CD system."}]
e4:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["In the next lesson, you will move from concept to practice by creating your first GitLab CI/CD pipeline and understanding the ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":".gitlab-ci.yml"}]," file that controls it."]}]
e5:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"knowledge-check","children":"Knowledge Check"}]
e6:["$","$Lfb",null,{"exercise":{"id":"intro-cicd-q1","title":"Question 1: Continuous Integration","description":"What is the main purpose of Continuous Integration?","options":[{"id":"a","text":"To delay merging code until a release manager approves it"},{"id":"b","text":"To merge code frequently and automatically validate it with builds and tests"},{"id":"c","text":"To deploy every code change directly to production without checks"},{"id":"d","text":"To replace version control with a deployment dashboard"}],"correctId":"b","explanation":"Continuous Integration is about merging code regularly and using automation to build and test changes quickly so integration problems are caught early.","wrongExplanations":{"a":"CI encourages frequent integration rather than delaying merges until the end of a release cycle.","c":"Automatic production deployment belongs to Continuous Deployment, not Continuous Integration by itself.","d":"CI works alongside version control systems such as Git; it does not replace them."}}}]
e7:["$","$Lfb",null,{"exercise":{"id":"intro-cicd-q2","title":"Question 2: Delivery vs Deployment","description":"Which statement correctly distinguishes Continuous Delivery from Continuous Deployment?","options":[{"id":"a","text":"Continuous Delivery and Continuous Deployment mean exactly the same thing"},{"id":"b","text":"Continuous Delivery runs tests, while Continuous Deployment does not"},{"id":"c","text":"Continuous Delivery keeps software ready to release, while Continuous Deployment automatically releases every passing change"},{"id":"d","text":"Continuous Delivery is only for mobile apps, while Continuous Deployment is only for web apps"}],"correctId":"c","explanation":"Continuous Delivery keeps the application in a deployable state and usually leaves the final production release as a manual step, while Continuous Deployment removes that final gate.","wrongExplanations":{"a":"They are closely related, but Continuous Deployment goes one step further by automatically releasing passing changes.","b":"Both practices depend heavily on automated validation such as builds and tests.","d":"The difference is about release automation, not application type."}}}]
e8:["$","$Lfb",null,{"exercise":{"id":"intro-cicd-q3","title":"Question 3: Pipeline Fundamentals","description":"In a typical CI/CD pipeline, what is the relationship between stages and jobs?","options":[{"id":"a","text":"A job contains several pipelines, and a stage stores secrets"},{"id":"b","text":"A stage is a phase such as build or test, and it contains one or more jobs that do the actual work"},{"id":"c","text":"A stage is another name for a Git branch, and a job is a merge request"},{"id":"d","text":"Stages are optional comments, while jobs are the only part GitLab reads"}],"correctId":"b","explanation":"Stages organize a pipeline into logical phases, and jobs are the individual units of work executed inside those phases.","wrongExplanations":{"a":"Pipelines contain stages, and stages contain jobs; secrets are handled separately through variables or settings.","c":"Branches and merge requests can trigger pipelines, but they are not the same as stages or jobs.","d":"GitLab uses both stages and jobs to understand pipeline order and execution."}}}]
e9:["$","hr",null,{}]
ea:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"frequently-asked-questions","children":"Frequently Asked Questions"}]
eb:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"What is the difference between continuous integration, continuous delivery and continuous deployment?"}]}]
ec:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Continuous integration is about merging work frequently and verifying each change automatically. Every commit triggers a build and a test run, so integration problems surface within minutes instead of at the end of a sprint. Continuous delivery extends that so every change that passes is packaged and provably releasable, with the final push to production left as a human decision. Continuous deployment removes that last manual gate: anything passing the pipeline goes live automatically. The abbreviation CD is genuinely ambiguous and covers the second and third, which is why it is worth being explicit about which one a team means."}]
ed:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"Do I need CI/CD for a small project?"}]}]
ee:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The continuous integration half pays for itself almost immediately, even solo. A pipeline that runs your tests on every push takes an hour to set up and catches the class of mistake that is otherwise found by a user. The deployment half is more of a judgement call, if you deploy once a month by hand and it works, automating it can wait. A reasonable order is tests on every commit first, then automated builds, then automated deployment to staging, and production deployment last once you trust the earlier stages."}]
ef:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"How long should a CI pipeline take?"}]}]
f0:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Under ten minutes for the feedback developers wait on, and ideally under five. The number matters because it changes behaviour: past roughly fifteen minutes people stop waiting for results, start batching changes, and the fast feedback that justifies CI disappears. When a pipeline grows too slow, the usual wins are running independent jobs in parallel, caching dependencies between runs, and splitting long test suites so only the affected parts run on every commit while the full suite runs on merge."}]
f1:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"What is the difference between a pipeline, a stage and a job?"}]}]
f2:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A job is a single unit of work, run the tests, build the image, deploy to staging, executed by a runner in its own environment. A stage is a named group of jobs that run in parallel with each other, such as ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"build"}],", ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"test"}]," and ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"deploy"}],". A pipeline is the whole sequence of stages triggered by an event, usually a push or a merge request. Stages run in order and, by default, a stage only begins once every job in the previous one has succeeded."]}]
f3:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"Which CI/CD tool should I learn?"}]}]
f4:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["The concepts transfer almost completely, so the specific tool matters less than people expect. Stages, jobs, runners, artifacts and caching exist in all of them under slightly different names. GitLab CI/CD is a good one to learn first because the whole pipeline is defined in a single ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":".gitlab-ci.yml"}]," file in your repository and the platform includes the registry, runners and environments without extra setup. GitHub Actions is the other obvious choice given how much code lives on GitHub. Learning either one well makes picking up the other a matter of hours."]}]
f5:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"Does CI/CD mean deploying to production automatically?"}]}]
f6:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"No, and conflating the two puts a lot of teams off. Continuous integration is only about verifying changes automatically, and it delivers most of the value on its own. Automated deployment to production is a separate decision that depends on your test coverage, your ability to roll back quickly, and your appetite for risk. Plenty of mature teams run excellent CI with a deliberate human approval before production, and that is a perfectly legitimate place to stop."}]
f7:["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]
f8:["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":[["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/kubernetes/introduction-to-kubernetes","children":"Kubernetes"}]," GitOps delivery"]}]
f9:["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Strong declarative deployments"}]
fa:["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Not a full CI platform by itself"}]
28:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
26:null
2a:[["$","title","0",{"children":"What Is CI/CD? A Beginner's Guide to Pipelines"}],["$","meta","1",{"name":"description","content":"Learn what CI/CD means: how continuous integration, delivery and deployment differ, why teams need automation and what a modern pipeline looks like."}],["$","link","2",{"rel":"author","href":"https://devopslesson.com"}],["$","meta","3",{"name":"author","content":"DevOpsLesson Team"}],["$","meta","4",{"name":"keywords","content":"what is cicd, ci cd explained, continuous integration continuous delivery, continuous deployment vs continuous delivery, cicd pipeline explained, what is a cicd pipeline, cicd for beginners, devops cicd, gitlab cicd introduction, software deployment automation, gitlab cicd tutorial, gitlab ci cd tutorial, gitlab-ci.yml tutorial, gitlab pipeline tutorial, gitlab runner tutorial, gitlab ci beginners, gitlab ci stages and jobs, gitlab ci variables, gitlab ci docker, gitlab ci security scanning, gitlab ci deploy aws, gitlab ci artifacts, gitlab ci cache, gitlab ci rules, gitlab ci environments, gitlab ci testing, gitlab ci best practices, gitlab multi project pipelines, devsecops gitlab, gitlab pipeline example"}],["$","meta","5",{"name":"creator","content":"DevOpsLesson"}],["$","meta","6",{"name":"publisher","content":"DevOpsLesson"}],["$","meta","7",{"name":"robots","content":"index, follow"}],["$","meta","8",{"name":"googlebot","content":"index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1"}],["$","link","9",{"rel":"canonical","href":"https://devopslesson.com/tutorials/cicd/introduction-to-cicd"}],["$","meta","10",{"name":"google-site-verification","content":"v1hGowZeYvsOScm3hhGtqYv7ZG_yiFG-YbjDK"}],["$","meta","11",{"property":"og:title","content":"Introduction to CI/CD"}],["$","meta","12",{"property":"og:description","content":"Learn what CI/CD means: how continuous integration, delivery and deployment differ, why teams need automation and what a modern pipeline looks like."}],["$","meta","13",{"property":"og:url","content":"https://devopslesson.com/tutorials/cicd/introduction-to-cicd"}],["$","meta","14",{"property":"og:site_name","content":"DevOpsLesson"}],["$","meta","15",{"property":"og:image","content":"https://devopslesson.com/api/og?title=Introduction+to+CI%2FCD&eyebrow=CI%2FCD+with+GitLab+Tutorial"}],["$","meta","16",{"property":"og:image:width","content":"1200"}],["$","meta","17",{"property":"og:image:height","content":"630"}],["$","meta","18",{"property":"og:image:alt","content":"Introduction to CI/CD"}],["$","meta","19",{"property":"og:type","content":"article"}],["$","meta","20",{"property":"article:published_time","content":"2026-07-12"}],["$","meta","21",{"property":"article:modified_time","content":"2026-07-12"}],["$","meta","22",{"property":"article:author","content":"Vishvesh Patel"}],["$","meta","23",{"property":"article:tag","content":"what is cicd"}],["$","meta","24",{"property":"article:tag","content":"ci cd explained"}],["$","meta","25",{"property":"article:tag","content":"continuous integration continuous delivery"}],["$","meta","26",{"property":"article:tag","content":"continuous deployment vs continuous delivery"}],["$","meta","27",{"property":"article:tag","content":"cicd pipeline explained"}],["$","meta","28",{"property":"article:tag","content":"what is a cicd pipeline"}],["$","meta","29",{"property":"article:tag","content":"cicd for beginners"}],["$","meta","30",{"property":"article:tag","content":"devops cicd"}],["$","meta","31",{"property":"article:tag","content":"gitlab cicd introduction"}],["$","meta","32",{"property":"article:tag","content":"software deployment automation"}],["$","meta","33",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","34",{"name":"twitter:title","content":"Introduction to CI/CD"}],["$","meta","35",{"name":"twitter:description","content":"Learn what CI/CD means: how continuous integration, delivery and deployment differ, why teams need automation and what a modern pipeline looks like."}],"$Lfc","$Lfd","$Lfe","$Lff","$L100","$L101"]
102:I[86869,[],"IconMark"]
fc:["$","meta","36",{"name":"twitter:image","content":"https://devopslesson.com/api/og?title=Introduction+to+CI%2FCD&eyebrow=CI%2FCD+with+GitLab+Tutorial"}]
fd:["$","link","37",{"rel":"shortcut icon","href":"/favicon.svg"}]
fe:["$","link","38",{"rel":"icon","href":"/icon","type":"image/png","sizes":"32x32"}]
ff:["$","link","39",{"rel":"icon","href":"/favicon.svg","type":"image/svg+xml"}]
100:["$","link","40",{"rel":"apple-touch-icon","href":"/icon","sizes":"180x180","type":"image/png"}]
101:["$","$L102","41",{}
Learn what CI/CD means: how continuous integration, delivery and deployment differ, why teams need automation and what a modern pipeline looks like.
Create your first GitLab CI/CD pipeline: write a minimal three-stage .gitlab-ci.yml, understand GitLab Runners, trigger a pipeline and read the job output.
See how a GitLab pipeline is built: how pipelines, stages and jobs relate, what runners do, how needs breaks stage lockstep and where artifacts fit.