{"id":91,"date":"2025-12-31T12:00:00","date_gmt":"2025-12-31T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=91"},"modified":"2026-10-30T06:52:34","modified_gmt":"2026-10-30T06:52:34","slug":"gitlab-ci-cd-multi-language-pipelines-java-node-js-python-and-php","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=91","title":{"rendered":"GitLab CI\/CD Multi-Language Pipelines: Java, Node.js, Python, and PHP"},"content":{"rendered":"<h3 class=\"wp-block-heading\">Introduction: Container-Native Means Language-Agnostic<\/h3><p class=\"wp-block-paragraph\">After building my first GitLab pipeline for Java, I wondered if I&#8217;d need to learn completely different approaches for Node.js, Python, and PHP.<\/p><p class=\"wp-block-paragraph\">The answer surprised me: the pipeline structure is identical across all languages.<\/p><p class=\"wp-block-paragraph\">The container-native architecture means:<\/p><ul class=\"wp-block-list\"><li>No language-specific agent configuration<\/li><li>No tool installation on runners<\/li><li>Same YAML pattern for every language<\/li><li>Only the Docker image changes<\/li><\/ul><p class=\"wp-block-paragraph\">I built pipelines for 4 different languages to prove this. This article shows the technical details of how GitLab&#8217;s container model makes language differences irrelevant.<\/p><h3 class=\"wp-block-heading\">The Universal GitLab Pattern<\/h3><p class=\"wp-block-paragraph\">Every language pipeline follows this structure:<\/p><pre class=\"wp-block-code\"><code>stages:\n  - build\n  - test\n  - package\n  - deploy\n\nbuild-job:\n  stage: build\n  image: &lt;language-image&gt;\n  script:\n    - &lt;install-dependencies&gt;\n\ntest-job:\n  stage: test\n  image: &lt;language-image&gt;\n  script:\n    - &lt;run-tests&gt;\n\npackage-job:\n  stage: package\n  image: docker:20.10.16\n  services:\n    - docker:20.10.16-dind\n  script:\n    - docker build -t registry.gitlab.com\/$CI_PROJECT_PATH .\n    - docker push registry.gitlab.com\/$CI_PROJECT_PATH\n\ndeploy-job:\n  stage: deploy\n  image: amazon\/aws-cli\n  script:\n    - kubectl apply -f deployment.yaml<\/code><\/pre><p class=\"wp-block-paragraph\">What changes between languages: Only &lt;language-image&gt; and &lt;install-dependencies&gt;.<\/p><p class=\"wp-block-paragraph\">What stays identical: Stages, Docker packaging, Kubernetes deployment, security scanning.<\/p><h3 class=\"wp-block-heading\">Language 1: Java &#8211; Maven in Containers<\/h3><p class=\"wp-block-paragraph\">Repository: GitLab\/Java-project-2\/Java-project-2.yml<\/p><pre class=\"wp-block-code\"><code>stages:\n- build\n- test\n- package\n- docker-stage\n- deploy\n\nbuild-job:\n  stage: build\n  image: maven:latest\n  script:\n  - mvn clean package\n  artifacts:\n    name: war-project\n    paths:\n    - target\/my-webapp.war\n\ntest-job:\n  stage: test\n  image: maven:latest\n  script:\n  - mvn test\n  artifacts:\n    reports:\n      junit:\n      - target\/surefire-reports\/TEST-*.xml\n\nsast:\n  stage: test\ninclude:\n- template: Jobs\/SAST.gitlab-ci.yml\n\npackage-job:\n  stage: package\n  image: maven:latest\n  script:\n  - mvn deploy -s settings.xml\n\ndocker-job:\n  stage: docker-stage\n  image: docker:20.10.16\n  services:\n  - docker:20.10.16-dind\n  before_script:\n  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com\n  script:\n  - docker build -t registry.gitlab.com\/lowyiiii\/java-project .\n  - docker push registry.gitlab.com\/lowyiiii\/java-project\n\ndeploy-job:\n  stage: deploy\n  image:\n    name: amazon\/aws-cli\n    entrypoint: [ \"\" ]\n  before_script:\n  - curl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl\n  - chmod +x .\/kubectl\n  - mkdir -p $HOME\/bin &amp;&amp; cp .\/kubectl $HOME\/bin\/kubectl &amp;&amp; export PATH=$HOME\/bin:$PATH\n  script:\n  - aws configure set aws_access_key $AWS_ACCESS_KEY_ID\n  - kubectl config use-context \"$KUBE_CONTEXT\"\n  - kubectl apply -f Application.yaml<\/code><\/pre><h3 class=\"wp-block-heading\">Deep Dive: Maven Container Execution<\/h3><p class=\"wp-block-paragraph\">What happens when build-job runs:<\/p><p class=\"wp-block-paragraph\">Step 1: GitLab Runner pulls Maven image<\/p><pre class=\"wp-block-code\"><code>docker pull maven:latest\n\n# Downloads layers:\n# maven:latest -&gt; maven:3.9.6-eclipse-temurin-17\n#   Layer 1: Base OS (Ubuntu 22.04) - 28MB\n#   Layer 2: Java 17 - 195MB\n#   Layer 3: Maven 3.9.6 - 8MB\n#   Total: 231MB<\/code><\/pre><p class=\"wp-block-paragraph\">Step 2: Runner creates container with mounted workspace<\/p><pre class=\"wp-block-code\"><code>docker run \\\n  --name build-job-12345 \\\n  --volume \/builds\/lowyiiii\/java-project:\/builds\/lowyiiii\/java-project \\\n  --workdir \/builds\/lowyiiii\/java-project \\\n  --env CI_PROJECT_DIR=\/builds\/lowyiiii\/java-project \\\n  maven:latest \\\n  sh -c \"mvn clean package\"<\/code><\/pre><p class=\"wp-block-paragraph\">Container filesystem:<\/p><pre class=\"wp-block-code\"><code>Container:\n\/\n\u251c\u2500\u2500 usr\/\n\u2502   \u2514\u2500\u2500 share\/\n\u2502       \u2514\u2500\u2500 maven\/          # Maven installation\n\u2502           \u251c\u2500\u2500 bin\/mvn\n\u2502           \u2514\u2500\u2500 lib\/\n\u251c\u2500\u2500 opt\/\n\u2502   \u2514\u2500\u2500 java\/               # Java 17 installation\n\u2502       \u2514\u2500\u2500 openjdk\/\n\u2514\u2500\u2500 builds\/                 # Mounted from host\n    \u2514\u2500\u2500 lowyiiii\/\n        \u2514\u2500\u2500 java-project\/   # Your code here\n            \u251c\u2500\u2500 pom.xml\n            \u2514\u2500\u2500 src\/<\/code><\/pre><p class=\"wp-block-paragraph\">Step 3: Maven executes inside container<\/p><pre class=\"wp-block-code\"><code># Inside container:\ncd \/builds\/lowyiiii\/java-project\nmvn clean package\n\n# Maven process:\n1. Reads pom.xml\n2. Downloads dependencies to \/root\/.m2\/repository\/\n3. Compiles src\/ to target\/classes\/\n4. Packages to target\/my-webapp.war<\/code><\/pre><p class=\"wp-block-paragraph\">Step 4: Container stops, workspace persists<\/p><pre class=\"wp-block-code\"><code># Container exits\ndocker stop build-job-12345\ndocker rm build-job-12345\n\n# But workspace remains on host:\n\/builds\/lowyiiii\/java-project\/target\/my-webapp.war \u2705<\/code><\/pre><p class=\"wp-block-paragraph\">Step 5: Artifact upload<\/p><pre class=\"wp-block-code\"><code># GitLab Runner (on host):\ncd \/builds\/lowyiiii\/java-project\nzip artifacts.zip target\/my-webapp.war\ncurl -X POST \\\n  -H \"JOB-TOKEN: $CI_JOB_TOKEN\" \\\n  -F \"file=@artifacts.zip\" \\\n  https:\/\/gitlab.com\/api\/v4\/projects\/123\/jobs\/12345\/artifacts<\/code><\/pre><h3 class=\"wp-block-heading\">Language 2: Node.js &#8211; Global Image Declaration<\/h3><p class=\"wp-block-paragraph\">Repository: GitLab\/nodejs-project\/nodejs-project.yml<\/p><pre class=\"wp-block-code\"><code>image: node\n\nstages:\n- build\n- test\n- sonarqube\n- package\n- deploy\n\nbuild-job:\n  stage: build\n  script:\n  - npm install\n\ntest-job:\n  stage: test\n  script:\n  - npm install\n  - npm test\n\nsast:\n  stage: test\ninclude:\n- template: Jobs\/SAST.gitlab-ci.yml\n\nsonarqube-check:\n  stage: sonarqube\n  image:\n    name: sonarsource\/sonar-scanner-cli:latest\n    entrypoint: [ \"\" ]\n  variables:\n    SONAR_USER_HOME: \"${CI_PROJECT_DIR}\/.sonar\"\n    GIT_DEPTH: \"0\"\n  cache:\n    key: \"${CI_JOB_NAME}\"\n    paths:\n    - .sonar\/cache\n  script:\n  - sonar-scanner\n  allow_failure: true\n  rules:\n  - if: $CI_COMMIT_BRANCH == 'main'\n\npackage-docker:\n  stage: package\n  image: docker:20.10.16\n  services:\n  - docker:20.10.16-dind\n  before_script:\n  - echo \"$DOCKER_HUB_PASSWORD\" | docker login -u \"$DOCKER_HUB_USERNAME\" --password-stdin\n  script:\n  - docker build -t lowyiiii\/nodejs-project:latest .\n  - docker push lowyiiii\/nodejs-project:latest\n\ndeploy-job:\n  stage: deploy\n  image:\n    name: amazon\/aws-cli\n    entrypoint: [ \"\" ]\n  before_script:\n  - curl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl\n  - chmod +x .\/kubectl\n  - mkdir -p $HOME\/bin &amp;&amp; cp .\/kubectl $HOME\/bin\/ &amp;&amp; export PATH=$HOME\/bin:$PATH\n  script:\n  - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID\n  - kubectl apply -f Application.yaml<\/code><\/pre><h3 class=\"wp-block-heading\">Deep Dive: Global Image vs Job-Specific Image<\/h3><p class=\"wp-block-paragraph\">Global image declaration:<\/p><pre class=\"wp-block-code\"><code>image: node\n\nbuild-job:\n  script:\n  - npm install  # Runs in node:latest container<\/code><\/pre><p class=\"wp-block-paragraph\">What GitLab does:<\/p><pre class=\"wp-block-code\"><code># GitLab expands this internally to:\nbuild-job:\n  image: node:latest  # Inherited from global\n  script:\n  - npm install<\/code><\/pre><p class=\"wp-block-paragraph\">Override for specific jobs:<\/p><pre class=\"wp-block-code\"><code>image: node  # Global default\n\nbuild-job:\n  script:\n  - npm install  # Uses node:latest\n\nsonarqube-check:\n  image: sonarsource\/sonar-scanner-cli:latest  # Overrides global\n  script:\n  - sonar-scanner  # Uses SonarQube image, NOT node<\/code><\/pre><p class=\"wp-block-paragraph\">Container execution:<\/p><pre class=\"wp-block-code\"><code># build-job runs:\ndocker run --rm node:latest sh -c \"npm install\"\n\n# sonarqube-check runs:\ndocker run --rm sonarsource\/sonar-scanner-cli:latest sh -c \"sonar-scanner\"<\/code><\/pre><h3 class=\"wp-block-heading\">Deep Dive: Conditional Execution with Rules<\/h3><pre class=\"wp-block-code\"><code>sonarqube-check:\n  rules:\n  - if: $CI_COMMIT_BRANCH == 'main'<\/code><\/pre><p class=\"wp-block-paragraph\">How GitLab evaluates rules:<\/p><p class=\"wp-block-paragraph\">Scenario 1: Commit to main branch<\/p><pre class=\"wp-block-code\"><code>Commit: abc123\nBranch: main\nEvent: push\n\nGitLab evaluation:\n$CI_COMMIT_BRANCH = \"main\"\nCondition: if: $CI_COMMIT_BRANCH == 'main'\nResult: TRUE \u2705\nAction: Job runs<\/code><\/pre><p class=\"wp-block-paragraph\">Scenario 2: Commit to feature branch<\/p><pre class=\"wp-block-code\"><code>Commit: def456\nBranch: feature\/add-login\nEvent: push\n\nGitLab evaluation:\n$CI_COMMIT_BRANCH = \"feature\/add-login\"\nCondition: if: $CI_COMMIT_BRANCH == 'main'\nResult: FALSE \u274c\nAction: Job skipped<\/code><\/pre><p class=\"wp-block-paragraph\">Pipeline visualization:<\/p><pre class=\"wp-block-code\"><code>Main branch pipeline:\nbuild-job \u2192 test-job \u2192 sonarqube-check \u2192 package \u2192 deploy\n   \u2705          \u2705            \u2705            \u2705        \u2705\n\nFeature branch pipeline:\nbuild-job \u2192 test-job \u2192 package \u2192 deploy\n   \u2705          \u2705         \u2705        \u2705\n                \u2191\n         sonarqube-check skipped<\/code><\/pre><p class=\"wp-block-paragraph\">Advanced rules combinations:<\/p><pre class=\"wp-block-code\"><code># Example 1: Run only on main OR tags\nrules:\n  - if: $CI_COMMIT_BRANCH == 'main'\n  - if: $CI_COMMIT_TAG\n\n# Example 2: Manual on main, auto on others\nrules:\n  - if: $CI_COMMIT_BRANCH == 'main'\n    when: manual\n  - when: on_success\n\n# Example 3: Run on MR to main only\nrules:\n  - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'<\/code><\/pre><p class=\"wp-block-paragraph\">Rules vs only\/except (deprecated):<\/p><pre class=\"wp-block-code\"><code># Old syntax (deprecated):\nsonarqube-check:\n  only:\n    - main\n  except:\n    - tags\n\n# New syntax (recommended):\nsonarqube-check:\n  rules:\n    - if: $CI_COMMIT_BRANCH == 'main' &amp;&amp; $CI_COMMIT_TAG == null<\/code><\/pre><p class=\"wp-block-paragraph\">Why rules are better:<\/p><p class=\"wp-block-paragraph\">only\/except limitations:<\/p><ul class=\"wp-block-list\"><li>Can&#8217;t combine conditions with AND\/OR<\/li><li>Can&#8217;t set when: manual conditionally<\/li><li>Can&#8217;t use complex expressions<\/li><\/ul><p class=\"wp-block-paragraph\">rules advantages:<\/p><ul class=\"wp-block-list\"><li>Full boolean logic: &amp;&amp;, ||, ==, !=<\/li><li>Conditional when: manual\/on_success\/delayed<\/li><li>Access to all CI variables<\/li><li>More readable for complex conditions<\/li><\/ul><h3 class=\"wp-block-heading\">Language 3: Python &#8211; Shell Executor for Performance<\/h3><p class=\"wp-block-paragraph\">Repository: GitLab\/python-project\/python-project.yml<\/p><pre class=\"wp-block-code\"><code>stages:\n- build\n- test\n- sonarqube\n- package\n- deploy\n\nbuild-job:\n  stage: build\n  tags:\n  - ec2\n  - shell\n  script:\n  - pip install -r requirements.txt\n\nsast:\n  stage: test\ninclude:\n- template: Jobs\/SAST.gitlab-ci.yml\n\nsonarqube-check:\n  stage: sonarqube\n  image:\n    name: sonarsource\/sonar-scanner-cli:latest\n    entrypoint: [ \"\" ]\n  variables:\n    SONAR_USER_HOME: \"${CI_PROJECT_DIR}\/.sonar\"\n    GIT_DEPTH: \"0\"\n  cache:\n    key: \"${CI_JOB_NAME}\"\n    paths:\n    - .sonar\/cache\n  script:\n  - sonar-scanner\n  allow_failure: true\n  rules:\n  - if: $CI_COMMIT_BRANCH == 'main'\n\npackage-job:\n  stage: package\n  tags:\n  - ec2\n  - shell\n  script:\n  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com\n  - docker build -t registry.gitlab.com\/lowyiiii\/python-project .\n  - docker push registry.gitlab.com\/lowyiiii\/python-project\n\ndeploy-job:\n  stage: deploy\n  tags:\n  - ec2\n  - shell\n  script:\n  - aws configure set aws_access_key $AWS_ACCESS_KEY_ID\n  - kubectl apply -f Application.yaml<\/code><\/pre><h3 class=\"wp-block-heading\">Deep Dive: Shell Executor vs Docker Executor<\/h3><p class=\"wp-block-paragraph\">Docker Executor (Java, Node.js):<\/p><pre class=\"wp-block-code\"><code>build-job:\n  image: python:3.11\n  script:\n  - pip install -r requirements.txt<\/code><\/pre><p class=\"wp-block-paragraph\">What happens:<\/p><pre class=\"wp-block-code\"><code># GitLab Runner executes:\ndocker pull python:3.11\ndocker run \\\n  --rm \\\n  --volume \/builds\/project:\/builds\/project \\\n  python:3.11 \\\n  sh -c \"pip install -r requirements.txt\"\n\n# Execution time breakdown:\n# - Pull image: 5s (cached) or 30s (first time)\n# - Start container: 2s\n# - Run pip install: 15s\n# - Stop container: 1s\n# Total: 23s (cached) or 48s (first time)<\/code><\/pre><p class=\"wp-block-paragraph\">Shell Executor (Python):<\/p><pre class=\"wp-block-code\"><code>build-job:\n  tags:\n  - ec2\n  - shell\n  script:\n  - pip install -r requirements.txt<\/code><\/pre><p class=\"wp-block-paragraph\">What happens:<\/p><pre class=\"wp-block-code\"><code># GitLab Runner executes DIRECTLY on EC2:\ncd \/home\/gitlab-runner\/builds\/project\npip install -r requirements.txt\n\n# Execution time breakdown:\n# - No image pull: 0s\n# - No container start: 0s\n# - Run pip install: 15s\n# - No container cleanup: 0s\n# Total: 15s<\/code><\/pre><p class=\"wp-block-paragraph\">Performance comparison:<\/p><figure class=\"wp-block-table\"><table><thead><tr><th>Executor<\/th><th>First Run<\/th><th>Cached Run<\/th><th>Memory<\/th><th>Disk<\/th><\/tr><\/thead><tbody><tr><td>Docker<\/td><td>48s<\/td><td>23s<\/td><td>1.5GB<\/td><td>900MB (image)<\/td><\/tr><tr><td>Shell<\/td><td>15s<\/td><td>15s<\/td><td>500MB<\/td><td>0MB (no image)<\/td><\/tr><\/tbody><\/table><\/figure><p class=\"wp-block-paragraph\">When to use each:<\/p><p class=\"wp-block-paragraph\">Docker Executor &#8211; Use when:<\/p><ul class=\"wp-block-list\"><li>Need isolation between jobs<\/li><li>Different Python versions per project<\/li><li>Clean environment required<\/li><li>Multiple projects on same runner<\/li><\/ul><p class=\"wp-block-paragraph\">Shell Executor &#8211; Use when:<\/p><ul class=\"wp-block-list\"><li>Speed is critical<\/li><li>Same Python version for all projects<\/li><li>Runner dedicated to one project<\/li><li>Tools pre-installed (Docker, kubectl, aws-cli)<\/li><\/ul><h3 class=\"wp-block-heading\">Deep Dive: Mixed Executor Strategy<\/h3><pre class=\"wp-block-code\"><code># Shell for speed\nbuild-job:\n  tags: [ec2, shell]\n  script: pip install\n\n# Docker for isolation\nsonarqube-check:\n  image: sonarsource\/sonar-scanner-cli\n  script: sonar-scanner\n\n# Shell with Docker commands\npackage-job:\n  tags: [ec2, shell]\n  script: docker build<\/code><\/pre><p class=\"wp-block-paragraph\">Why this works:<\/p><p class=\"wp-block-paragraph\">Shell executor can run Docker commands:<\/p><pre class=\"wp-block-code\"><code># On EC2 runner (shell executor):\nwhich docker\n# Output: \/usr\/bin\/docker \u2705\n\n# GitLab job executes:\ndocker build -t myimage .\n# Works! Uses host Docker daemon<\/code><\/pre><p class=\"wp-block-paragraph\">Architecture:<\/p><pre class=\"wp-block-code\"><code>EC2 Instance (GitLab Runner)\n\u251c\u2500\u2500 GitLab Runner process\n\u251c\u2500\u2500 Docker daemon (running)\n\u251c\u2500\u2500 Python 3.11 (installed)\n\u251c\u2500\u2500 kubectl (installed)\n\u2514\u2500\u2500 aws-cli (installed)\n\nJob with shell executor:\n\u2192 Runs directly on EC2\n\u2192 Can use all installed tools\n\u2192 No container overhead\n\nJob with Docker executor:\n\u2192 Runner starts new container\n\u2192 Isolated from host\n\u2192 Tools only from image<\/code><\/pre><p class=\"wp-block-paragraph\">Performance benefit:<\/p><pre class=\"wp-block-code\"><code>Build pipeline (shell executor):\nbuild-job: 15s\npackage-job: 35s (docker build)\ndeploy-job: 8s\nTotal: 58s\n\nSame pipeline (docker executor):\nbuild-job: 23s (+ container overhead)\npackage-job: 45s (nested Docker complexity)\ndeploy-job: 15s (+ container overhead)\nTotal: 83s\n\nShell executor: 30% faster! \u2705<\/code><\/pre><h3 class=\"wp-block-heading\">Language 4: PHP &#8211; Composer and Lint Stage<\/h3><p class=\"wp-block-paragraph\">Repository: GitLab\/php-project\/php-project.yml<\/p><pre class=\"wp-block-code\"><code>stages:\n- build\n- lint\n- test\n- package\n- deploy\n\nbuild:\n  stage: build\n  image: composer:latest\n  tags:\n  - ec2\n  - php\n  script:\n  - composer install\n\nlint:\n  stage: lint\n  tags:\n  - ec2\n  - php\n  script:\n  - php -l public\/index.php\n\nsast:\n  stage: test\ninclude:\n- template: Jobs\/SAST.gitlab-ci.yml\n\npackage:\n  stage: package\n  image: docker:20.10.16\n  services:\n  - docker:20.10.16-dind\n  before_script:\n  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.gitlab.com\n  script:\n  - docker build -t registry.gitlab.com\/lowyiiii\/php-test-project .\n  - docker push registry.gitlab.com\/lowyiiii\/php-test-project\n\ndeploy:\n  stage: deploy\n  tags:\n  - ec2\n  - php\n  image: amazon\/aws-cli\n  entrypoint: [ \"\" ]\n  before_script:\n  - curl -O https:\/\/s3.us-west-2.amazonaws.com\/amazon-eks\/1.30.2\/2024-07-12\/bin\/linux\/amd64\/kubectl\n  - chmod +x .\/kubectl\n  - mkdir -p $HOME\/bin &amp;&amp; cp .\/kubectl $HOME\/bin\/kubectl &amp;&amp; export PATH=$HOME\/bin:$PATH\n  script:\n  - aws configure set aws_access_key $AWS_ACCESS_KEY_ID\n  - kubectl apply -f Application.yaml<\/code><\/pre><h3 class=\"wp-block-heading\">Deep Dive: PHP Syntax Linting<\/h3><pre class=\"wp-block-code\"><code>lint:\n  script:\n  - php -l public\/index.php<\/code><\/pre><p class=\"wp-block-paragraph\">What php -l does:<\/p><pre class=\"wp-block-code\"><code>php -l public\/index.php\n\n# PHP process:\n1. Reads file: public\/index.php\n2. Lexical analysis (tokenization)\n3. Syntax parsing\n4. Reports errors WITHOUT executing code<\/code><\/pre><p class=\"wp-block-paragraph\">Example 1: Valid PHP<\/p><pre class=\"wp-block-code\"><code>&lt;?php\n\/\/ public\/index.php\nfunction getUserName($id) {\n    return \"User \" . $id;\n}\necho getUserName(123);\n?&gt;\nphp -l public\/index.php\n# Output: No syntax errors detected in public\/index.php\n# Exit code: 0 \u2705<\/code><\/pre><p class=\"wp-block-paragraph\">Example 2: Missing semicolon<\/p><pre class=\"wp-block-code\"><code>&lt;?php\nfunction getUserName($id) {\n    return \"User \" . $id  \/\/ Missing semicolon!\n}\n?&gt;\nphp -l public\/index.php\n# Output:\n# Parse error: syntax error, unexpected '}', expecting ',' or ';' in public\/index.php on line 4\n# Exit code: 255 \u274c\n# Pipeline fails!<\/code><\/pre><p class=\"wp-block-paragraph\">Example 3: Missing bracket<\/p><pre class=\"wp-block-code\"><code>&lt;?php\nfunction getUserName($id) {\n    if ($id &gt; 0) {\n        return \"User \" . $id;\n    \/\/ Missing closing bracket\n}\n?&gt;\nphp -l public\/index.php\n# Output:\n# Parse error: syntax error, unexpected end of file in public\/index.php on line 6\n# Exit code: 255 \u274c<\/code><\/pre><p class=\"wp-block-paragraph\">Why this is PHP-specific:<\/p><p class=\"wp-block-paragraph\">JavaScript\/Node.js:<\/p><pre class=\"wp-block-code\"><code># No separate lint command needed\nnode app.js\n# Checks syntax automatically when executing<\/code><\/pre><p class=\"wp-block-paragraph\">Python:<\/p><pre class=\"wp-block-code\"><code># No separate lint command needed\npython app.py\n# Checks syntax automatically when executing<\/code><\/pre><p class=\"wp-block-paragraph\">PHP:<\/p><pre class=\"wp-block-code\"><code># php app.php would execute code!\n# Need -l flag for syntax-only check\nphp -l app.php\n# Only checks syntax, no execution<\/code><\/pre><p class=\"wp-block-paragraph\">Lint stage benefit:<\/p><pre class=\"wp-block-code\"><code>Without lint stage:\nbuild \u2192 test \u2192 package \u2192 deploy \u2192 ERROR: Syntax error!\nTime wasted: 5 minutes\n\nWith lint stage:\nbuild \u2192 lint \u2192 ERROR: Syntax error!\nTime wasted: 30 seconds\n\nSaves 4.5 minutes per syntax error! \u2705<\/code><\/pre><h3 class=\"wp-block-heading\">Deep Dive: Composer Container Image<\/h3><pre class=\"wp-block-code\"><code>build:\n  image: composer:latest\n  script:\n  - composer install<\/code><\/pre><p class=\"wp-block-paragraph\">What&#8217;s in composer:latest image:<\/p><pre class=\"wp-block-code\"><code># Simplified composer Dockerfile\nFROM php:8.2-cli-alpine\n\n# Install Composer\nCOPY --from=composer:2 \/usr\/bin\/composer \/usr\/bin\/composer\n\n# Install common PHP extensions\nRUN docker-php-ext-install pdo_mysql mbstring\n\n# Set working directory\nWORKDIR \/app\n\nENTRYPOINT [\"composer\"]\nCMD [\"--help\"]<\/code><\/pre><p class=\"wp-block-paragraph\">Image contents:<\/p><pre class=\"wp-block-code\"><code>composer:latest\n\u251c\u2500\u2500 PHP 8.2 CLI\n\u251c\u2500\u2500 Composer 2.6\n\u251c\u2500\u2500 Extensions:\n\u2502   \u251c\u2500\u2500 pdo_mysql (database)\n\u2502   \u251c\u2500\u2500 mbstring (multibyte strings)\n\u2502   \u251c\u2500\u2500 zip (compression)\n\u2502   \u2514\u2500\u2500 curl (HTTP requests)\n\u2514\u2500\u2500 Size: ~150MB (Alpine-based)<\/code><\/pre><p class=\"wp-block-paragraph\">What composer install does inside container:<\/p><pre class=\"wp-block-code\"><code># Inside composer:latest container:\ncd \/builds\/project\ncomposer install\n\n# Composer process:\n1. Reads composer.json and composer.lock\n2. Downloads packages from packagist.org\n3. Extracts to vendor\/ directory\n4. Generates autoload files:\n   - vendor\/autoload.php\n   - vendor\/composer\/autoload_*.php\n5. Runs post-install scripts (if any)<\/code><\/pre><p class=\"wp-block-paragraph\">composer.lock ensures consistency:<\/p><pre class=\"wp-block-code\"><code>\/\/ composer.lock\n{\n  \"packages\": [\n    {\n      \"name\": \"symfony\/http-foundation\",\n      \"version\": \"v6.0.8\",\n      \"source\": {\n        \"type\": \"git\",\n        \"url\": \"https:\/\/github.com\/symfony\/http-foundation.git\",\n        \"reference\": \"c9c86b7b9313ee7d8dd9d632a65bb97a440bc7d3\"\n      }\n    }\n  ]\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Every build installs EXACT same version (commit hash c9c86b7&#8230;)!<\/p><h3 class=\"wp-block-heading\">Universal Patterns Across All Languages<\/h3><h3 class=\"wp-block-heading\">Pattern 1: Built-in SAST<\/h3><pre class=\"wp-block-code\"><code>sast:\n  stage: test\ninclude:\n- template: Jobs\/SAST.gitlab-ci.yml<\/code><\/pre><p class=\"wp-block-paragraph\">Works identically for all 4 languages:<\/p><figure class=\"wp-block-table\"><table><thead><tr><th>Language<\/th><th>SAST Analyzer<\/th><th>Detects<\/th><\/tr><\/thead><tbody><tr><td>Java<\/td><td>SpotBugs<\/td><td>SQL injection, XXE, insecure crypto<\/td><\/tr><tr><td>Node.js<\/td><td>Semgrep<\/td><td>Prototype pollution, command injection<\/td><\/tr><tr><td>Python<\/td><td>Bandit<\/td><td>Pickle exploits, SQL injection, hardcoded secrets<\/td><\/tr><tr><td>PHP<\/td><td>phpcs-security-audit<\/td><td>SQL injection, XSS, file inclusion<\/td><\/tr><\/tbody><\/table><\/figure><p class=\"wp-block-paragraph\">Same YAML, different analyzers automatically selected!<\/p><h3 class=\"wp-block-heading\">Pattern 2: Docker Build Stage<\/h3><pre class=\"wp-block-code\"><code>package:\n  image: docker:20.10.16\n  services:\n  - docker:20.10.16-dind\n  script:\n  - docker build -t registry.gitlab.com\/$CI_PROJECT_PATH .\n  - docker push registry.gitlab.com\/$CI_PROJECT_PATH<\/code><\/pre><p class=\"wp-block-paragraph\">Identical for all languages. Only Dockerfile differs:<\/p><p class=\"wp-block-paragraph\">Java Dockerfile:<\/p><pre class=\"wp-block-code\"><code>FROM tomcat:9-jdk11\nCOPY target\/*.war \/usr\/local\/tomcat\/webapps\/\nEXPOSE 8080<\/code><\/pre><p class=\"wp-block-paragraph\">Node.js Dockerfile:<\/p><pre class=\"wp-block-code\"><code>FROM node:18-alpine\nWORKDIR \/app\nCOPY package*.json .\/\nRUN npm ci --only=production\nCOPY . .\nEXPOSE 3000\nCMD [\"npm\", \"start\"]<\/code><\/pre><p class=\"wp-block-paragraph\">Python Dockerfile:<\/p><pre class=\"wp-block-code\"><code>FROM python:3.11-slim\nWORKDIR \/app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nEXPOSE 5000\nCMD [\"gunicorn\", \"app:app\"]<\/code><\/pre><p class=\"wp-block-paragraph\">PHP Dockerfile:<\/p><pre class=\"wp-block-code\"><code>FROM php:8.2-apache\nCOPY . \/var\/www\/html\/\nRUN docker-php-ext-install pdo_mysql\nEXPOSE 80<\/code><\/pre><h3 class=\"wp-block-heading\">Pattern 3: Kubernetes Deployment<\/h3><pre class=\"wp-block-code\"><code>deploy:\n  image: amazon\/aws-cli\n  script:\n  - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID\n  - kubectl apply -f Application.yaml<\/code><\/pre><p class=\"wp-block-paragraph\">Identical command for all languages!<\/p><p class=\"wp-block-paragraph\">Only Application.yaml differs:<\/p><pre class=\"wp-block-code\"><code># All languages use same structure\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: &lt;app-name&gt;\nspec:\n  replicas: 3\n  template:\n    spec:\n      containers:\n      - name: app\n        image: registry.gitlab.com\/&lt;project-path&gt;\n        ports:\n        - containerPort: &lt;port&gt;  # 8080\/3000\/5000\/80<\/code><\/pre><h3 class=\"wp-block-heading\">Performance Comparison: All 4 Languages<\/h3><p class=\"wp-block-paragraph\">From my home lab testing (same EC2 t3.medium):<\/p><h3 class=\"wp-block-heading\">Build Time Breakdown<\/h3><h3 class=\"wp-block-heading\">Key Lessons: Why Container-Native Works<\/h3><h3 class=\"wp-block-heading\">1. Language Tools in Images, Not on Runners<\/h3><p class=\"wp-block-paragraph\">Traditional CI (Jenkins):<\/p><p class=\"wp-block-paragraph\">Agent setup required:<\/p><ul class=\"wp-block-list\"><li>Install Java 11<\/li><li>Install Maven 3.9<\/li><li>Install Node.js 18<\/li><li>Install Python 3.11<\/li><li>Install PHP 8.2<\/li><li>Install Composer<\/li><\/ul><p class=\"wp-block-paragraph\">Maintenance required:<\/p><ul class=\"wp-block-list\"><li>Update Java<\/li><li>Update Maven<\/li><li>Update Node.js<\/li><li>Update Python<\/li><li>Update PHP<\/li><li>Update Composer<\/li><\/ul><p class=\"wp-block-paragraph\">GitLab CI:<\/p><p class=\"wp-block-paragraph\">Runner setup:<\/p><ul class=\"wp-block-list\"><li>Install GitLab Runner<\/li><li>Install Docker<\/li><\/ul><p class=\"wp-block-paragraph\">That&#8217;s it! \u2705<\/p><p class=\"wp-block-paragraph\">Tools come from images:<\/p><ul class=\"wp-block-list\"><li>maven:3.9-jdk-11<\/li><li>node:18<\/li><li>python:3.11<\/li><li>composer:latest<\/li><\/ul><h3 class=\"wp-block-heading\">2. Same Pipeline Pattern Everywhere<\/h3><pre class=\"wp-block-code\"><code># This pattern works for EVERY language:\nbuild \u2192 test \u2192 sast \u2192 package \u2192 deploy\n\nOnly changes:\n- image: &lt;language&gt;\n- script: &lt;package-manager&gt; install<\/code><\/pre><h3 class=\"wp-block-heading\">3. Built-in Features are Language-Agnostic<\/h3><p class=\"wp-block-paragraph\">SAST, JUnit reports, artifacts, caching &#8211; all work the same way across all languages.<\/p><h3 class=\"wp-block-heading\">4. Mixed Executors Optimize Performance<\/h3><pre class=\"wp-block-code\"><code>Use Docker when: isolation needed\nUse Shell when: speed needed\nMix both: best performance \u2705<\/code><\/pre>","protected":false},"excerpt":{"rendered":"<p>Introduction: Container-Native Means Language-Agnostic After building my first GitLab pipeline for Java, I wondered if I&#8217;d need to learn completely different approaches&hellip;<\/p>\n","protected":false},"author":2,"featured_media":94,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"2","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/gitlab-cicd-multi-language-pipelines-java-nodejs-python-reza-chegini-fg4ac\/","footnotes":""},"categories":[27,10],"tags":[30,8,48,45,46,47,25],"series":[40],"class_list":["post-91","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ci-cd","category-devops","tag-ci-cd","tag-devops","tag-gitlab-ci-cd","tag-java","tag-node-js","tag-php","tag-python","series-gitlab-ci-cd"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/gitlab-cicd-multi-language-pipelines-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/91","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=91"}],"version-history":[{"count":2,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/91\/revisions"}],"predecessor-version":[{"id":93,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/91\/revisions\/93"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=91"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=91"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=91"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=91"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}