{"id":1804,"date":"2026-10-28T06:05:27","date_gmt":"2026-10-28T06:05:27","guid":{"rendered":"https:\/\/devopsschool.org\/blog\/?p=1804"},"modified":"2026-10-28T06:05:28","modified_gmt":"2026-10-28T06:05:28","slug":"gitlab-ci-cd-basics-for-devops-students-a-hands-on-beginners-guide","status":"publish","type":"post","link":"https:\/\/devopsschool.org\/blog\/gitlab-ci-cd-basics-for-devops-students-a-hands-on-beginners-guide\/","title":{"rendered":"GitLab CI\/CD Basics for DevOps Students: A Hands-On Beginner\u2019s Guide"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"572\" src=\"https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-20.png\" alt=\"\" class=\"wp-image-1805\" srcset=\"https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-20.png 1024w, https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-20-300x168.png 300w, https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-20-768x429.png 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">1. Introduction to GitLab CI\/CD<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab CI\/CD is a built-in feature of GitLab that automates the building, testing, and deployment of applications. Whenever changes are pushed to a Git repository, GitLab can trigger an automated sequence of tasks\u2014known as a pipeline\u2014to validate the code and ensure it meets quality standards before it ever reaches a staging or production server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">DevOps teams rely on GitLab CI\/CD because it establishes a repeatable, auditable software delivery process. Automation eliminates human errors such as forgetting to run unit tests, missing an environment configuration step, or deploying outdated binaries. It also provides immediate feedback: instead of discovering a defect days after merging code, developers learn within minutes whether their changes broke existing functionality.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At its core, GitLab CI\/CD links your source code to automated execution environments:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Git Repository:<\/strong> Houses the application code and the pipeline definition file (<code>.gitlab-ci.yml<\/code>).<\/li>\n\n\n\n<li><strong>Pipelines:<\/strong> The top-level engine that triggers whenever code events (like commits, merges, or tags) occur.<\/li>\n\n\n\n<li><strong>Jobs and Stages:<\/strong> Individual units of work (such as compiling code or running a linter) structured into chronological phases.<\/li>\n\n\n\n<li><strong>Runners:<\/strong> Independent agents that pick up these jobs and execute them inside isolated operating systems or containers.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">2. Understanding the Basics of CI\/CD<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before writing configuration files, it helps to dissect the acronyms that define modern release engineering: Continuous Integration, Continuous Delivery, and Continuous Deployment. While closely related, they represent distinct milestones in the DevOps lifecycle.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>+--------------------------------------------------------------------------+\n|                        CONTINUOUS INTEGRATION (CI)                       |\n|  &#091; Developer Commit ] ---&gt; &#091; Automated Build ] ---&gt; &#091; Automated Tests ]  |\n+--------------------------------------------------------------------------+\n                                     |\n                                     v\n+--------------------------------------------------------------------------+\n|                        CONTINUOUS DELIVERY (CD)                          |\n|  &#091; Package Artifacts ] ---&gt; &#091; Staging Deploy ] ---&gt; &#091; Manual Approval ]  |\n+--------------------------------------------------------------------------+\n                                     |\n                                     v\n+--------------------------------------------------------------------------+\n|                       CONTINUOUS DEPLOYMENT (CD)                         |\n|            &#091; Fully Automated Deployment to Production ]                  |\n+--------------------------------------------------------------------------+\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Continuous Integration (CI)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Continuous Integration is the practice of frequently merging code changes into a central repository, usually multiple times a day. Each merge or push triggers an automated build and test sequence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The primary goal of CI is early bug detection. If two developers introduce conflicting changes or a new library breaks an existing unit test, the pipeline flags the issue immediately, isolating the problem while the context is fresh in the engineer&#8217;s mind.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Continuous Delivery (CD)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Continuous Delivery picks up where CI ends. Once code passes all automated tests, Continuous Delivery ensures that the application is automatically packaged, configured, and staged so that it is always in a release-ready state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In a Continuous Delivery workflow, deploying to production requires an explicit human decision\u2014such as clicking a &#8220;Deploy&#8221; button or approving a release ticket. This model is common in enterprises with scheduled release windows, strict compliance audits, or business-driven rollout timelines.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Continuous Deployment (CD)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Continuous Deployment removes the manual intervention step entirely. Every change that passes the complete battery of automated tests and validation stages flows directly and automatically into the production environment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This model demands an exceptionally mature test suite, comprehensive automated monitoring, and reliable rollback mechanisms. It is widely adopted by high-velocity cloud companies that deploy updates dozens or hundreds of times per day.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How CI\/CD Fits into the DevOps Lifecycle<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In a traditional lifecycle, development and operations operate in silos. Developers write software over weeks or months and hand it off to operations teams to manually deploy, leading to friction and delayed troubleshooting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CI\/CD bridges this gap. When a developer pushes a commit:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>GitLab detects the Git push event via webhooks.<\/li>\n\n\n\n<li>The CI pipeline compiles the source code and runs unit, integration, and security scans.<\/li>\n\n\n\n<li>If any test fails, the pipeline halts, alerting the developer via notifications and inline merge request warnings.<\/li>\n\n\n\n<li>If all tests pass, the CD stage builds container images or deployment bundles.<\/li>\n\n\n\n<li>The application is deployed to test or production servers automatically or upon approval.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">3. What Is GitLab CI\/CD and How Does It Work?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab CI\/CD functions as a coordinated ecosystem composed of several distinct components. Understanding how these pieces communicate makes writing configurations and debugging failures straightforward.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GitLab Projects and Repositories<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Your project repository on GitLab serves as the single source of truth. It stores your application source code, configuration files, documentation, and your CI\/CD configuration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The <code>.gitlab-ci.yml<\/code> Configuration File<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The entire CI\/CD pipeline is managed as code using a YAML file named <code class=\"\">.gitlab-ci.yml<\/code>, placed in the root directory of your repository. This file defines:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Which stages the pipeline contains.<\/li>\n\n\n\n<li>What commands run for each job.<\/li>\n\n\n\n<li>What conditions dictate when a job runs (branches, tags, merge requests).<\/li>\n\n\n\n<li>Which container images or environments are required.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Because this configuration lives inside your Git repository alongside your code, it is version-controlled, auditable, and branches can modify pipeline behaviors independently.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pipelines, Stages, and Jobs<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Job:<\/strong> The smallest building block of a pipeline. It defines a specific task, such as compiling code, running a test suite, or pushing an artifact. A job specifies the commands to execute (<code>script<\/code>).<\/li>\n\n\n\n<li><strong>Stage:<\/strong> A logical grouping of jobs that run at the same time. Common stages include <code>build<\/code>, <code>test<\/code>, and <code>deploy<\/code>.<\/li>\n\n\n\n<li><strong>Pipeline:<\/strong> The overarching structure that ties stages together. By default, stages execute sequentially: jobs in the <code>test<\/code> stage will not run until all jobs in the <code>build<\/code> stage finish successfully. If any job in an earlier stage fails, the pipeline terminates early to prevent bad code from moving downstream.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">GitLab Runners and Executors<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab itself (the web interface and Git server) does not run your test commands or build your software. Instead, it delegates those workloads to independent processes called <strong>GitLab Runners<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A runner is an agent installed on a virtual machine, bare-metal server, or container cluster. When a pipeline is created, GitLab assigns jobs to available runners. The runner uses an <strong>executor<\/strong>\u2014such as a Docker engine, a Kubernetes pod, or a local Shell environment\u2014to provision an execution context, run the declared commands, capture the output, and send the result back to GitLab.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Artifacts, Caches, and Job Logs<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Job Logs:<\/strong> The real-time console terminal output generated by the runner during command execution. These logs are vital for diagnosing syntax errors, missing packages, or assertion failures.<\/li>\n\n\n\n<li><strong>Artifacts:<\/strong> Files or directories generated by a job (such as a compiled binary, a code coverage report, or a <code>.zip<\/code> archive) that are preserved and uploaded back to GitLab. Other jobs in subsequent stages can download these artifacts to continue the workflow.<\/li>\n\n\n\n<li><strong>Cache:<\/strong> A performance-optimization tool that stores project dependencies (like <code>node_modules<\/code> or <code>pip<\/code> packages) between pipeline runs, eliminating the need to download large external libraries every time.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Environment Variables and Deployment Environments<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">CI\/CD processes frequently require configuration values, API endpoints, and authentication credentials. Rather than hardcoding these into scripts, GitLab provides <strong>CI\/CD Variables<\/strong> that inject values dynamically into the runner&#8217;s runtime environment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab also features <strong>Environments<\/strong>, allowing you to track deployments across multiple tiers (such as <code>development<\/code>, <code>staging<\/code>, and <code>production<\/code>) directly from the web interface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Setting Up a Beginner-Friendly GitLab CI\/CD Project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Getting started requires no specialized infrastructure if you use GitLab&#8217;s hosted environment. Here is how to configure a starter project from scratch:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Create a GitLab Account and Project<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Log in to <a href=\"https:\/\/gitlab.com\/\" target=\"_blank\" rel=\"noopener\">GitLab.com<\/a> (or your educational institution&#8217;s self-hosted instance).<\/li>\n\n\n\n<li>Click <strong>New project \/ repository<\/strong> on your dashboard.<\/li>\n\n\n\n<li>Select <strong>Create blank project<\/strong>.<\/li>\n\n\n\n<li>Name your project (e.g., <code>devops-cicd-starter<\/code>), set its visibility level to <strong>Private<\/strong> or <strong>Public<\/strong>, ensure <strong>Initialize repository with a README<\/strong> is checked, and click <strong>Create project<\/strong>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Understand Runner Availability<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before a pipeline can run, your project must have access to a runner. On GitLab.com, projects generally have access to <strong>Shared Runners<\/strong> (also called GitLab-hosted runners) within free-tier compute quota limits.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are using a self-hosted GitLab instance managed by a university or company, administrators may have already configured shared runners for you. You can verify runner access by navigating to <strong>Settings &gt; CI\/CD &gt; Runners<\/strong> in your project sidebar. If shared runners are enabled, you are ready to build pipelines. If not, a project runner must be registered on an accessible machine.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Create the Pipeline Configuration<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>In your project repository view, click the <strong>+<\/strong> (plus) icon next to the branch selector and choose <strong>New file<\/strong>.<\/li>\n\n\n\n<li>Set the file name to <code>.gitlab-ci.yml<\/code>.<\/li>\n\n\n\n<li>Paste in your pipeline configuration (detailed in the next section).<\/li>\n\n\n\n<li>Enter a descriptive commit message, such as <code>ci: add initial pipeline configuration<\/code>.<\/li>\n\n\n\n<li>Ensure the target branch is your default branch (<code>main<\/code>) and click <strong>Commit changes<\/strong>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Monitor the Execution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Once committed, GitLab automatically detects the file and starts the pipeline:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Navigate to <strong>Build &gt; Pipelines<\/strong> in the left sidebar.<\/li>\n\n\n\n<li>You will see a newly triggered pipeline marked with a status icon (e.g., <code>running<\/code>, <code>passed<\/code>, or <code>failed<\/code>).<\/li>\n\n\n\n<li>Click on the pipeline ID or status badge to open the graphical pipeline visualizer.<\/li>\n\n\n\n<li>Click on an individual job (such as <code>build-job<\/code>) to open the live terminal log and watch the runner execute your commands.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">5. Creating Your First GitLab CI\/CD Pipeline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s inspect a complete, working <code>.gitlab-ci.yml<\/code> file designed for a beginner. This example defines a lightweight Node.js project pipeline with three sequential stages: <code>build<\/code>, <code>test<\/code>, and <code class=\"\">deploy<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Define the base Docker image used by default for all jobs\nimage: node:18-alpine\n\n# Define the sequential stages of the pipeline\nstages:\n  - build\n  - test\n  - deploy\n\n# Job 1: Compile or package the application\ncompile_code:\n  stage: build\n  script:\n    - echo \"=== Starting Build Stage ===\"\n    - node -v\n    - npm -v\n    - echo \"Packaging application assets...\"\n    - mkdir -p dist\n    - echo \"console.log('App initialized successfully');\" &gt; dist\/bundle.js\n    - echo \"Build completed successfully.\"\n  artifacts:\n    paths:\n      - dist\/\n    expire_in: 1 hour\n\n# Job 2: Execute automated tests against the build output\nrun_unit_tests:\n  stage: test\n  script:\n    - echo \"=== Starting Test Stage ===\"\n    - test -f dist\/bundle.js\n    - echo \"Running unit tests...\"\n    - node dist\/bundle.js\n    - echo \"All tests passed cleanly.\"\n\n# Job 3: Execute a simulated deployment\nsimulate_deployment:\n  stage: deploy\n  script:\n    - echo \"=== Starting Deployment Stage ===\"\n    - echo \"Deploying bundle to staging server...\"\n    - cat dist\/bundle.js\n    - echo \"Deployment verified.\"\n  rules:\n    - if: '$CI_COMMIT_BRANCH == \"main\"'\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Dissecting the Configuration File<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\"><code>image<\/code><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The top-level <code>image<\/code> keyword instructs the runner&#8217;s Docker executor to pull a specific container image from Docker Hub (<code>node:18-alpine<\/code> in this case). Every job runs inside a clean, fresh instance of this lightweight Linux environment, ensuring tools like <code>node<\/code> and <code>npm<\/code> are pre-installed.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><code>stages<\/code><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>stages<\/code> array defines the execution order. Here, all jobs belonging to the <code>build<\/code> stage run first. Once they succeed, the pipeline advances to <code>test<\/code>. When tests pass, it proceeds to <code>deploy<\/code>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Job Names (<code>compile_code<\/code>, <code>run_unit_tests<\/code>, <code>simulate_deployment<\/code>)<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">These are human-readable identifiers that appear as interactive nodes in the GitLab web interface. You can name jobs anything descriptive, though using snake_case or lowercase hyphens is standard practice.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><code>stage<\/code><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Inside each job block, the <code>stage<\/code> keyword maps that job to one of the entries declared in the global <code>stages<\/code> list. If you assign a job to a stage that was not declared, GitLab throws a validation error.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><code>script<\/code><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>script<\/code> section contains the actual shell commands executed by the runner. If any command exits with a non-zero exit code (the standard Linux indicator of a command failure), the job immediately halts and fails.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><code>artifacts<\/code><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">In our <code>compile_code<\/code> job, we generate a directory called <code>dist\/<\/code>. By declaring it under <code>artifacts.paths<\/code>, GitLab preserves this directory after the job completes and uploads it back to the GitLab coordinator. When <code>run_unit_tests<\/code> starts in the next stage, GitLab automatically downloads and unpacks <code>dist\/<\/code> into the runner&#8217;s workspace.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><code>rules<\/code><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>rules<\/code> keyword controls when jobs are allowed to execute. In our <code>simulate_deployment<\/code> job, the rule:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rules:\n  - if: '$CI_COMMIT_BRANCH == \"main\"'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">ensures that deployment is only attempted if the commit was made to or merged into the <code>main<\/code> branch. If a developer pushes code to a feature branch (<code>feature\/login<\/code>), the <code>compile_code<\/code> and <code>run_unit_tests<\/code> jobs will run, but <code>simulate_deployment<\/code> will be skipped.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How Job Failures Impact Execution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pipeline stages act as quality gates. If an assertion fails inside the <code>run_unit_tests<\/code> job (for example, if <code>test -f dist\/bundle.js<\/code> does not find the file), the runner reports a non-zero exit status.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab marks <code>run_unit_tests<\/code> as failed and halts the pipeline immediately. The subsequent stage\u2014<code>deploy<\/code>\u2014is canceled. This prevents broken code or missing builds from reaching target environments.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. GitLab CI\/CD Pipeline Execution: A Step-by-Step Walkthrough<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To appreciate how CI\/CD works in practice, let us trace what happens when an engineer pushes code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#091;Developer: git push]\n        |\n        v\n&#091;GitLab Coordinator] &lt;---&gt; Parses .gitlab-ci.yml &amp; creates Pipeline\n        |\n        v\n&#091;Job Queue] (Stage: build)\n        |\n        v\n&#091;GitLab Runner] pulls job, spins up Docker container, executes script\n        |\n        +---&gt; Success: Uploads logs &amp; artifacts\n        |\n        v\n&#091;GitLab Coordinator] moves to next stage (Stage: test)\n        |\n        +---&gt; If Failure: Halts pipeline, marks red, alerts team\n<\/code><\/pre>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Trigger:<\/strong> A developer runs <code>git push origin main<\/code>.<\/li>\n\n\n\n<li><strong>Parsing:<\/strong> The GitLab server receives the commits, checks for a <code>.gitlab-ci.yml<\/code> file, validates the YAML syntax, and constructs an executable pipeline object containing defined stages and dependencies.<\/li>\n\n\n\n<li><strong>Queueing:<\/strong> The pipeline marks stage 1 (<code>build<\/code>) as pending and places its jobs into a central queue.<\/li>\n\n\n\n<li><strong>Pickup:<\/strong> An active runner listening for jobs requests work from the GitLab coordinator. GitLab assigns <code>compile_code<\/code> to the runner.<\/li>\n\n\n\n<li><strong>Execution:<\/strong> The runner connects to its executor (e.g., Docker), pulls the requested image (<code>node:18-alpine<\/code>), clones the repository code at the exact commit hash, and runs each shell command specified in <code>script<\/code>.<\/li>\n\n\n\n<li><strong>Reporting:<\/strong> The runner streams stdout and stderr logs back to GitLab in real time. Upon command completion, it compresses specified artifacts (<code>dist\/<\/code>), uploads them to GitLab, and reports a status code (<code>0<\/code> for success).<\/li>\n\n\n\n<li><strong>Progression:<\/strong> GitLab evaluates the stage. Because all jobs in <code>build<\/code> succeeded, it advances to <code>test<\/code>, passes along artifacts, and assigns <code>run_unit_tests<\/code> to a runner.<\/li>\n\n\n\n<li><strong>Final State:<\/strong> When all stages finish, the pipeline status turns green (<code>passed<\/code>). If any command had returned an error, the pipeline would turn red (<code>failed<\/code>), sending an email alert to the committer.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">What to Check When a Pipeline Fails<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a red <code>failed<\/code> badge appears, do not panic. Systematic debugging will resolve most beginner errors:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Open the Job Output:<\/strong> Click directly on the failed red job icon to open the console terminal log.<\/li>\n\n\n\n<li><strong>Scroll to the Bottom:<\/strong> The root cause is almost always printed in the last 15\u201320 lines of the console log, highlighted in red text.<\/li>\n\n\n\n<li><strong>Verify Local Reproduction:<\/strong> Run the identical command that failed (e.g., <code>npm test<\/code> or a compile command) inside your local terminal to see if the problem exists in your code or is unique to the CI environment.<\/li>\n\n\n\n<li><strong>Check the Execution Context:<\/strong> Ensure the container image you selected contains the binaries your scripts invoke.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">7. Understanding GitLab Runners<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A common misconception among beginners is that GitLab CI runs commands inside GitLab&#8217;s web server. In reality, pipelines rely entirely on <strong>GitLab Runners<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A GitLab Runner is an open-source application written in Go that can run on Linux, macOS, Windows, or inside a container orchestration platform like Kubernetes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>+-------------------------------------------------------------+\n|                      GITLAB SERVER                          |\n|  - Stores Code Repository                                   |\n|  - Coordinates Pipelines                                    |\n|  - Tracks Job Status, Artifacts, and Environment Variables   |\n+-------------------------------------------------------------+\n                             ^\n                             |  HTTPS Polling (Long Poll)\n                             v\n+-------------------------------------------------------------+\n|                      GITLAB RUNNER                          |\n|  - Lightweight Go process installed on VM\/Host              |\n|  - Periodically asks GitLab: \"Do you have work for me?\"     |\n|  - Dispatches job to selected executor                      |\n+-------------------------------------------------------------+\n                             |\n         +-------------------+-------------------+\n         |                                       |\n         v                                       v\n+------------------+                   +------------------+\n|  Shell Executor  |                   | Docker Executor  |\n| Directly runs    |                   | Spins up clean   |\n| bash\/sh on the   |                   | container per    |\n| host system      |                   | job              |\n+------------------+                   +------------------+\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Common Runner Executors<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The runner uses an <strong>executor<\/strong> to establish the runtime environment:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Docker Executor (Recommended for most workloads):<\/strong> The runner starts a clean, isolated Docker container using the image defined in your YAML. Once the job completes, the container is destroyed. This guarantees a clean environment free of leftover temporary files from previous runs.<\/li>\n\n\n\n<li><strong>Shell Executor:<\/strong> The runner executes job scripts directly on the host machine&#8217;s native shell (Bash, PowerShell). While easy to configure, it is less secure and less predictable because files and background processes can bleed between different pipeline runs, and all required software dependencies must be installed manually on the host server.<\/li>\n\n\n\n<li><strong>Kubernetes Executor:<\/strong> Spins up dedicated pods inside a Kubernetes cluster for each pipeline job, enabling massive horizontal scaling for enterprise engineering teams.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Shared Runners vs. Project-Specific Runners<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Shared Runners:<\/strong> Maintained by GitLab (on GitLab.com) or central IT infrastructure teams. They are available to all projects within an organization, running jobs on generic, secure compute pools.<\/li>\n\n\n\n<li><strong>Project-Specific (Specific) Runners:<\/strong> Installed and registered by a developer for a single project. This is necessary if your jobs require specialized hardware (like GPUs), internal network access to private data centers, or custom software that cannot run inside generic containers.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Common Runner Pitfalls<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>No Runners Available \/ Stuck in Pending:<\/strong> If your pipeline shows a perpetual &#8220;stuck&#8221; or &#8220;pending&#8221; state, your project either has no assigned runners, runner quotas have been exhausted, or the runner tags do not match the job requirements.<\/li>\n\n\n\n<li><strong>Missing Tools in Minimal Images:<\/strong> Beginners often specify minimal base images (like <code>alpine<\/code> or <code>slim<\/code>) and encounter errors like <code>curl: not found<\/code> or <code>git: command not found<\/code>. Minimal images only contain bare operating system utilities; you must install additional dependencies via package managers (such as <code>apk add --no-cache curl<\/code>) before using them.<\/li>\n\n\n\n<li><strong>Host Permission Issues:<\/strong> On Shell executors, the user account running the <code>gitlab-runner<\/code> daemon may lack permissions to write to destination directories or access system services like Docker.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">8. Essential GitLab CI\/CD Features for Students<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Beyond running basic shell scripts, GitLab provides native features that transform simple scripts into resilient automation pipelines.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Job Artifacts and Expiration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Artifacts pass generated files between stages or save them for manual download from the GitLab UI. Setting an expiration date prevents build files from consuming your repository storage allowance indefinitely:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>compile_binary:\n  stage: build\n  script:\n    - go build -o bin\/server main.go\n  artifacts:\n    name: \"server-binary-$CI_COMMIT_SHORT_SHA\"\n    paths:\n      - bin\/\n    expire_in: 3 days\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. Caching Dependencies<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Unlike artifacts (which pass new build outputs between stages), the <strong>cache<\/strong> preserves external vendor libraries across completely separate pipeline runs. Caching avoids re-downloading packages repeatedly, cutting build times significantly:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cache:\n  key: ${CI_COMMIT_REF_SLUG}\n  paths:\n    - .npm\/\n\nbefore_script:\n  - npm ci --cache .npm --prefer-offline\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. Predefined and Custom Environment Variables<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab automatically injects useful metadata into every job as environment variables:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>$CI_COMMIT_BRANCH<\/code>: The branch being built (e.g., <code>main<\/code>).<\/li>\n\n\n\n<li><code>$CI_COMMIT_SHORT_SHA<\/code>: The first eight characters of the commit hash.<\/li>\n\n\n\n<li><code>$CI_JOB_STAGE<\/code>: The name of the currently executing stage.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">You can also define custom variables directly inside your YAML:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>variables:\n  APP_ENV: \"testing\"\n  DATABASE_PORT: \"5432\"\n\ntest_connection:\n  stage: test\n  script:\n    - echo \"Testing connection to database on port $DATABASE_PORT in $APP_ENV mode\"\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4. Secure CI\/CD Variables for Credentials<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Passwords, cloud API keys, and deployment secrets must never be placed inside <code>.gitlab-ci.yml<\/code>. Instead, store them in the GitLab web interface under <strong>Settings &gt; CI\/CD &gt; Variables<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once saved, GitLab injects them into the runner environment securely at runtime. You can mark variables as <strong>Masked<\/strong> (hiding their values if accidentally printed in logs) and <strong>Protected<\/strong> (ensuring they are only injected into pipelines running on protected branches like <code class=\"\">main<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>deploy_to_cloud:\n  stage: deploy\n  script:\n    # $CLOUD_API_TOKEN is stored securely in GitLab Settings, not in this file\n    - curl --header \"Authorization: Bearer $CLOUD_API_TOKEN\" https:\/\/api.myhosting.com\/deploy\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">5. Manual Jobs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some pipeline actions should not happen automatically. Adding <code>when: manual<\/code> creates an interactive button in the GitLab interface, requiring human approval before the job proceeds:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>production_release:\n  stage: deploy\n  script:\n    - echo \"Executing irreversible production release...\"\n  when: manual\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">9. Building a Practical Student Project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To consolidate these concepts, let us build a practical, automated verification pipeline for a lightweight Python web application.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Project Objective and Prerequisites<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Objective:<\/strong> Create a repository with a Python module, validate the code using the <code>flake8<\/code> linter, run automated unit tests with <code>pytest<\/code>, package an artifact, and run a demonstration deployment job.<\/li>\n\n\n\n<li><strong>Prerequisites:<\/strong> A GitLab account with shared runners enabled, and Git installed locally.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Suggested Repository Structure<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create the following file tree on your machine:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python-cicd-demo\/\n\u251c\u2500\u2500 .gitlab-ci.yml\n\u251c\u2500\u2500 app\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2514\u2500\u2500 calculator.py\n\u251c\u2500\u2500 tests\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2514\u2500\u2500 test_calculator.py\n\u2514\u2500\u2500 requirements.txt\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Application Code (<code>app\/calculator.py<\/code>)<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Python<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def add(a, b):\n    return a + b\n\n\ndef subtract(a, b):\n    return a - b\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Unit Tests (<code>tests\/test_calculator.py<\/code>)<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Python<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from app.calculator import add, subtract\n\n\ndef test_add():\n    assert add(2, 3) == 5\n    assert add(-1, 1) == 0\n\n\ndef test_subtract():\n    assert subtract(5, 3) == 2\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Dependencies (<code>requirements.txt<\/code>)<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>flake8==6.1.0\npytest==7.4.2\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">The Pipeline Configuration (<code>.gitlab-ci.yml<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create your <code class=\"\">.gitlab-ci.yml<\/code> file with the following configuration:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Use official Python runtime based on Alpine Linux\nimage: python:3.11-alpine\n\n# Define our execution stages\nstages:\n  - lint\n  - test\n  - package\n  - preview_deploy\n\n# Global cache configuration to preserve Python wheels between runs\ncache:\n  key: python-dependencies\n  paths:\n    - .cache\/pip\n\n# Common setup run before every job script\nbefore_script:\n  - python --version\n  - pip install --cache-dir .cache\/pip -r requirements.txt\n\n# Stage 1: Static Code Analysis (Linting)\nlinting_job:\n  stage: lint\n  script:\n    - echo \"Running flake8 code style checks...\"\n    - flake8 app\/ tests\/ --max-line-length=88\n\n# Stage 2: Automated Testing\ntesting_job:\n  stage: test\n  script:\n    - echo \"Executing pytest test suite...\"\n    - pytest -v tests\/\n\n# Stage 3: Packaging Assets\npackage_job:\n  stage: package\n  script:\n    - echo \"Creating release bundle...\"\n    - mkdir -p build_output\n    - cp -r app\/ build_output\/\n    - echo \"Commit: $CI_COMMIT_SHORT_SHA\" &gt; build_output\/version.txt\n    - tar -czf release-$CI_COMMIT_SHORT_SHA.tar.gz -C build_output .\n  artifacts:\n    name: \"release-archive\"\n    paths:\n      - release-*.tar.gz\n    expire_in: 2 days\n\n# Stage 4: Demonstration Deployment (Non-Production Simulation)\nstaging_deploy_simulation:\n  stage: preview_deploy\n  script:\n    - echo \"=== DEMONSTRATION DEPLOYMENT ===\"\n    - echo \"This job demonstrates a CD release step.\"\n    - echo \"Deploying release archive to preview environment...\"\n    - ls -la release-*.tar.gz\n    - echo \"Verification complete: Application ready for preview.\"\n  rules:\n    - if: '$CI_COMMIT_BRANCH == \"main\"'\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Implementation Steps<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Initialize the project locally:Bash<code>git init python-cicd-demo cd python-cicd-demo<\/code><\/li>\n\n\n\n<li>Create the files and directory structure shown above, adding the Python code and configuration.<\/li>\n\n\n\n<li>Commit the changes:Bash<code>git add . git commit -m \"feat: initial commit with app, tests, and CI pipeline\"<\/code><\/li>\n\n\n\n<li>Create a new blank project named <code>python-cicd-demo<\/code> on GitLab.<\/li>\n\n\n\n<li>Link your local repository and push:Bash<code>git remote add origin https:\/\/gitlab.com\/&lt;your-username&gt;\/python-cicd-demo.git git branch -M main git push -u origin main<\/code><\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Expected Pipeline Behavior<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Navigate to <strong>Build &gt; Pipelines<\/strong> in your repository. You will see a four-stage pipeline:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><code>linting_job<\/code> runs first inside Python 3.11, ensuring formatting complies with standard PEP 8 rules.<\/li>\n\n\n\n<li>Once linting succeeds, <code>testing_job<\/code> runs the unit tests inside <code>tests\/test_calculator.py<\/code>.<\/li>\n\n\n\n<li>After tests pass, <code>package_job<\/code> creates a compressed tarball containing the application code and commit metadata, uploading it as a downloadable artifact.<\/li>\n\n\n\n<li>Finally, because the branch is <code>main<\/code>, <code>staging_deploy_simulation<\/code> executes the mock deployment step.<\/li>\n<\/ol>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Note on Deployment Realism:<\/strong> In production systems, a deployment job does not merely run <code>echo<\/code> commands. It connects over SSH, invokes tools like Ansible or Terraform, pushes container images to a container registry, or updates Kubernetes manifests via Helm or <code>kubectl<\/code>. This simulation demonstrates the lifecycle structure and variable handling before introducing cloud infrastructure costs.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">Common Errors in This Project<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Lint Failures:<\/strong> If you add extra trailing spaces or blank lines in <code>calculator.py<\/code>, <code>flake8<\/code> will return an error code and fail the <code>linting_job<\/code>. Open the job log to see the exact file and line number flagged.<\/li>\n\n\n\n<li><strong>Assertion Errors:<\/strong> If you change <code>assert add(2, 3) == 5<\/code> to <code>assert add(2, 3) == 6<\/code>, <code class=\"\">pytest<\/code> will report an assertion failure and abort the pipeline before packaging begins.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">10. Common GitLab CI\/CD Errors and How to Troubleshoot Them<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><td><strong>Symptom \/ Error Log<\/strong><\/td><td><strong>Likely Root Cause<\/strong><\/td><td><strong>Investigation &amp; Resolution<\/strong><\/td><\/tr><\/thead><tbody><tr><td><strong><code>yaml invalid: mapping values are not allowed here<\/code><\/strong><\/td><td>YAML indentation syntax error (e.g., using tabs instead of spaces, or uneven indentation).<\/td><td>Copy your file contents into GitLab&#8217;s built-in <strong>CI Lint<\/strong> tool (<strong>Build &gt; Pipeline editor &gt; CI lint<\/strong>). Ensure you use standard 2-space indentation and no tabs.<\/td><\/tr><tr><td><strong><code>job failed: job assigned to stage 'x' that is not in stages list<\/code><\/strong><\/td><td>A job specifies a <code>stage:<\/code> value that is omitted or misspelled in the global <code>stages:<\/code> block.<\/td><td>Check the top-level <code>stages:<\/code> array in <code>.gitlab-ci.yml<\/code>. Verify spelling and capitalization match the job&#8217;s <code>stage<\/code> property exactly.<\/td><\/tr><tr><td><strong><code>This job is stuck because you do not have any runners online assigned to this project<\/code><\/strong><\/td><td>No shared runners are available, your usage quota is exhausted, or the job requires runner tags that no active runner has.<\/td><td>Go to <strong>Settings &gt; CI\/CD &gt; Runners<\/strong>. Confirm shared runners are toggled on. If your job specifies <code>tags:<\/code>, verify a registered runner possesses those identical tags.<\/td><\/tr><tr><td><strong><code>\/bin\/sh: eval: line X: docker: not found<\/code> (or command not found)<\/strong><\/td><td>The Docker image specified for the job does not contain the CLI utility invoked in your <code>script<\/code>.<\/td><td>Switch to an image containing the tool (e.g., <code>docker:cli<\/code> or <code>node:18<\/code>), or add an installation command (e.g., <code>apk add --no-cache curl<\/code> or <code>apt-get install -y git<\/code>) in <code>before_script<\/code>.<\/td><\/tr><tr><td><strong><code>ERROR: Job failed: exit code 1<\/code><\/strong><\/td><td>A command inside your <code>script<\/code> returned an unhandled error (such as a failed unit test or syntax error).<\/td><td>Open the failed job log, scroll up from the bottom line, and locate the command that emitted the non-zero status code. Fix the underlying code defect or script argument.<\/td><\/tr><tr><td><strong><code>Missing variable: $API_TOKEN<\/code> (or blank output)<\/strong><\/td><td>A CI\/CD variable is misspelled or blocked by protection rules.<\/td><td>Check <strong>Settings &gt; CI\/CD &gt; Variables<\/strong>. Ensure the variable name matches your script. If marked as <strong>Protected<\/strong>, the pipeline must run on a protected branch or tag to access it.<\/td><\/tr><tr><td><strong><code>WARNING: dist\/: no matching files<\/code><\/strong><\/td><td>The path specified in <code>artifacts.paths<\/code> was not created by the build script, or was generated in a different relative path.<\/td><td>Ensure previous commands in <code>script<\/code> actually generate the declared folder or file in the current working directory. Check for relative path mismatches.<\/td><\/tr><tr><td><strong><code>Permission denied (publickey)<\/code> during deployment<\/strong><\/td><td>The runner SSH key is missing, incorrect, or lacks file permissions.<\/td><td>Ensure your private SSH key is stored as a <code>File<\/code> or <code>Variable<\/code> type in GitLab CI variables and that permissions are set via <code>chmod 600 ~\/.ssh\/id_rsa<\/code> before running SSH commands.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">11. GitLab CI\/CD Security Best Practices<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Automated pipelines have access to deployment environments, infrastructure credentials, and your code. Neglecting pipeline security can expose your entire infrastructure to risk.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>+---------------------------------------------------------------------------------+\n|                       PIPELINE SECURITY BEST PRACTICES                          |\n+---------------------------------------------------------------------------------+\n| 1. NEVER HARDCODE SECRETS     | Store tokens\/keys in Settings &gt; CI\/CD Variables |\n| 2. MASK SENSITIVE VARIABLES   | Obfuscates secret values in console logs        |\n| 3. PROTECTED BRANCHES ONLY    | Keep production secrets away from feature forks |\n| 4. LEAST PRIVILEGE            | Use scoped API tokens with minimal permissions  |\n| 5. SANITIZE LOG OUTPUT        | Avoid \"echo $SECRET\" or \"set -x\" debugging      |\n+---------------------------------------------------------------------------------+\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">1. Never Commit Plaintext Secrets<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Never commit passwords, cloud service tokens, database connection strings, or private SSH keys to your Git repository. Once committed, secrets remain in Git history even if deleted in a later commit. Always use GitLab&#8217;s encrypted <strong>CI\/CD Variables<\/strong> interface.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Mask and Protect Sensitive Variables<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When creating a variable in the GitLab settings:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Masked:<\/strong> Check this box so that if a script accidentally outputs the variable, GitLab automatically replaces the secret with <code>[MASKED]<\/code> in the job logs.<\/li>\n\n\n\n<li><strong>Protected:<\/strong> Check this box to ensure the variable is only decrypted and injected into jobs running on protected branches (such as <code>main<\/code>). This prevents contributors on experimental feature branches from reading production secrets.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">3. Practice Least Privilege<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Do not use root administrative credentials in your pipelines. If a job only needs to push images to a container registry, create an API token with read\/write access limited specifically to that registry, rather than an account-wide administrative token.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Review Pipelines on Merge Requests<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Untrusted code pushed to a public project could modify <code>.gitlab-ci.yml<\/code> to exfiltrate environment variables. Restrict external merge request pipelines, require code reviews from senior maintainers before pipelines execute on forks, and avoid using unvetted third-party container images.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Prevent Secret Leakage in Debugging<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Avoid turning on full shell execution tracing (<code>set -x<\/code>) in scripts that process sensitive data, as this prints expanded variables directly to stdout. Similarly, do not write test scripts that print all environment variables (<code>printenv<\/code> or <code>env<\/code>) to the console.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">12. GitLab CI\/CD Best Practices for DevOps Students<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Writing an initial pipeline is easy; writing maintainable, efficient, and reliable pipelines requires intentional habits:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Use Intuitive, Descriptive Names:<\/strong> Avoid naming jobs <code>job1<\/code> or <code>run<\/code>. Use descriptive identifiers like <code>lint_python_code<\/code>, <code>build_docker_image<\/code>, or <code>run_integration_tests<\/code>. This makes pipeline graphs instantly understandable to team members.<\/li>\n\n\n\n<li><strong>Keep Jobs Focused and Atomic:<\/strong> A single job should do one thing well. Avoid combining linting, unit testing, compiling, and deploying into a single script block. Splitting these into separate jobs enables parallel execution and tells you instantly which phase failed.<\/li>\n\n\n\n<li><strong>Don&#8217;t Repeat Yourself (DRY):<\/strong> Use YAML anchors (<code>&amp;<\/code> and <code class=\"\">*<\/code>) or the <code>extends<\/code> keyword to reuse boilerplate configuration across multiple jobs:YAML<code>.python_template: &amp;python_base image: python:3.11-alpine before_script: - pip install -r requirements.txt test_unit: &lt;&lt;: *python_base stage: test script: - pytest tests\/unit test_integration: &lt;&lt;: *python_base stage: test script: - pytest tests\/integration<\/code><\/li>\n\n\n\n<li><strong>Control Execution Frequency:<\/strong> Don&#8217;t run long, resource-intensive deployment pipelines on every single commit to an experimental branch. Use <code>rules<\/code> to run full end-to-end suites only on merge requests or commits merged into <code>main<\/code>.<\/li>\n\n\n\n<li><strong>Read Your Logs Carefully:<\/strong> Treat logs as the primary source of truth. When troubleshooting, read the exact console output rather than guessing. Take note of command exit statuses and warnings.<\/li>\n\n\n\n<li><strong>Document Your Pipeline Variables:<\/strong> If your pipeline requires specific CI\/CD variables to run successfully, document their required names, formats, and purposes in your project&#8217;s <code>README.md<\/code>.<\/li>\n\n\n\n<li><strong>Iterate Incrementally:<\/strong> When learning, do not copy a 200-line enterprise pipeline you do not understand. Start with a single stage and a single <code>echo<\/code> command. Verify it turns green, then add another job, then add artifacts, then introduce testing tools. Step-by-step iteration builds real competence.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">13. GitLab CI\/CD Interview Preparation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use these concise questions and answers to prepare for beginner DevOps and junior cloud engineer interviews:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is GitLab CI\/CD?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab CI\/CD is an automated workflow engine built directly into GitLab. It automatically builds, tests, and deploys applications whenever changes are pushed to a Git repository, ensuring code quality and rapid, reliable software releases.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the purpose of the <code>.gitlab-ci.yml<\/code> file?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>.gitlab-ci.yml<\/code> file is the pipeline configuration file stored in the root directory of a GitLab repository. It defines the stages, jobs, container images, environment variables, commands, and conditions that govern how automated tasks execute.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between a stage and a job?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>job<\/strong> is an individual execution unit that contains specific shell commands to execute (e.g., <code>npm test<\/code>). A <strong>stage<\/strong> is a chronological grouping of jobs (e.g., <code>build<\/code>, <code>test<\/code>, <code>deploy<\/code>). Jobs belonging to the same stage can run in parallel, while stages run sequentially.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is a GitLab Runner?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A GitLab Runner is an independent, lightweight agent installed on a server or container cluster that connects to GitLab, retrieves pending jobs from the queue, executes their commands within an executor (such as Docker), and streams logs and results back to GitLab.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between artifacts and caches?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Artifacts<\/strong> are files generated by a job (such as compiled binaries or test reports) that are uploaded to GitLab and passed downstream to subsequent stages of the same pipeline. A <strong>cache<\/strong> stores external dependencies (like downloaded language packages) across different pipeline runs to accelerate build speeds.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do you troubleshoot a failed pipeline?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">First, open the visual pipeline graph and click on the failed job to view the console logs. Second, scroll to the bottom of the log to identify the failed command and error code. Third, reproduce the command locally to isolate whether the failure stems from application code, missing dependencies, or CI environment configurations.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How can CI\/CD variables help protect sensitive information?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">CI\/CD variables allow developers to store sensitive secrets (passwords, tokens, keys) in the encrypted GitLab database instead of committing them in plaintext inside source code. Variables can also be masked in terminal logs and restricted so they only decrypt on protected branches.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How is Continuous Delivery different from Continuous Deployment?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In Continuous Delivery, every passing code change is automatically built, tested, and packaged into a release-ready state, but the final deployment to production requires a manual trigger or approval. In Continuous Deployment, the final deployment to production is fully automated without any human intervention.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">14. Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Is GitLab CI\/CD suitable for beginners?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Because it uses a single YAML configuration file and requires no external server installation when using hosted runners on GitLab.com, it is widely considered one of the most accessible CI\/CD platforms for students and beginners.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do students need a paid GitLab plan to learn CI\/CD?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. The free tier of GitLab.com includes shared runner compute minutes each month, which is more than enough for building learning projects, running unit tests, and practicing configuration syntax. Students can also register their own free local runner on their personal laptop.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can GitLab CI\/CD work with Docker?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. GitLab CI\/CD has deep native Docker integration. You can run jobs inside custom Docker containers by specifying the <code>image<\/code> keyword, and you can build and publish new Docker images directly inside your pipeline using Docker-in-Docker (dind) or Kaniko.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What happens when a pipeline job fails?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">By default, if any command in a job returns a non-zero exit status, the runner halts the job immediately, marks it as failed, and alerts GitLab. GitLab stops the pipeline, canceling any upcoming stages to prevent defective code from advancing downstream.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How can students practice GitLab CI\/CD without deploying to a live server?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You do not need a paid cloud server to learn CI\/CD. You can practice all core concepts by compiling source code, executing unit and integration tests, performing static code analysis (linting), building container images, creating zip artifacts, and running mock deployment scripts that log output.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What should a beginner learn before GitLab CI\/CD?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before diving into CI\/CD, you should have a solid foundation in basic Git commands (<code>clone<\/code>, <code>branch<\/code>, <code>commit<\/code>, <code>push<\/code>, <code>merge<\/code>), basic Linux shell navigation (<code>ls<\/code>, <code>cd<\/code>, environment variables, exit codes), and basic familiarity with whatever programming language or project you are building.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How can GitLab CI\/CD skills support a DevOps career?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitLab is widely used across start-ups, enterprises, and government agencies. Demonstrating that you understand pipeline architecture, infrastructure-as-code, automated testing workflows, and secure credential handling gives you a distinct advantage in technical interviews for junior DevOps, cloud, and site reliability engineering roles.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">15. Conclusion: Start Practising GitLab CI\/CD<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mastering GitLab CI\/CD bridges the gap between writing isolated code and delivering reliable, production-ready software through automated pipelines, runners, and declarative <code>.gitlab-ci.yml<\/code> workflows. True proficiency comes from hands-on iteration: take an existing project, configure a basic build and test pipeline from scratch, deliberately trigger and fix errors, and gradually incorporate artifacts, caching, and secure variables. Building these muscle memories step-by-step transforms theoretical DevOps concepts into practical engineering skills that stand out in real-world environments and technical interviews.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>1. Introduction to GitLab CI\/CD GitLab CI\/CD is a built-in feature of GitLab that automates the building, testing, and deployment of applications. Whenever changes are pushed to a Git repository,&hellip;<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1804","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts\/1804","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/comments?post=1804"}],"version-history":[{"count":1,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts\/1804\/revisions"}],"predecessor-version":[{"id":1806,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts\/1804\/revisions\/1806"}],"wp:attachment":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/media?parent=1804"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/categories?post=1804"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/tags?post=1804"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}