Skip to main content
Please wait...
Image

Homemade CI/CD: All-You-Can-Eat Buffet, Gourmet Food Truck… or Fine Dining?

At OBI Partner, we love coding in Python and PHP. But let’s be honest: the moment everything goes live in production often looks more like a comedy sketch than a well-prepared kitchen recipe.
Between smoking servers, crying databases, and developers swearing “but it works on my machine”… we had to find a better method.

That’s where self-hosted CI/CD comes in. No nosy SaaS platforms, no dependency on moody clouds: everything runs at home, in your own infra.

And the good news: I’ve cooked up several recipes for you 🔥.

🥘 Option A : GitLab CE ( All-You-Can-Eat buffet )

GitLab is the Chinese buffet of DevOps: it has everything. Forge, issues, merge requests, pipelines, Docker registry, monitoring… the only thing missing are the spring rolls.

Why you’ll love it

  • All-in-one: one interface, no need to chase after 10 tools.

  • Powerful pipeline: GitLab Runner does it all (Docker, Kubernetes, SSH).

  • Integrated registry: no hacks needed to store images.

  • Huge ecosystem: ask for help and you’re never alone.

Why you’ll suffer with it

  • RAM consumption: GitLab is an ogre. If you don’t feed it 16 GB, it looks at you like you tried serving it a salad.

  • Monolithic: you get the whole package, even if you don’t need half of it.

docker-compose GitLab CE

version: '3.7'
services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    restart: always
    hostname: gitlab.local
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://gitlab.local'
        gitlab_rails['gitlab_shell_ssh_port'] = 2222
    ports:
      - "8080:80"
      - "443:443"
      - "2222:22"
    volumes:
      - ./gitlab/config:/etc/gitlab
      - ./gitlab/logs:/var/log/gitlab
      - ./gitlab/data:/var/opt/gitlab

  runner:
    image: gitlab/gitlab-runner:latest
    restart: always
    volumes:
      - ./runner/config:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock

(Pro tip: run it on a dedicated server, otherwise it’ll strangle your other services like a hungry boa.)

🌮 Option B: Gitea + Woodpecker + Harbor (The Gourmet Food Truck)

If GitLab is the buffet, then Gitea + Woodpecker + Harbor is the gourmet food truck. Each tool is light, fast, and you pick exactly what goes on your plate.

Why you’ll love it

  • Super light: Gitea starts faster than a microwave.

  • Modular: grab only what you need.

  • Woodpecker CI: readable, easy-to-maintain pipelines.

  • Harbor: registry with built-in scanner (your digital bodyguard).

Why you’ll suffer with it

  • More plumbing: you need to connect the bricks yourself.

  • Fewer built-in tools: no advanced project management like GitLab.

docker-compose Gitea + Woodpecker + Harbor

version: '3.8'

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always
    volumes:
      - ./gitea:/data
    ports:
      - "3000:3000"
      - "222:22"

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker-server
    environment:
      - WOODPECKER_OPEN=true
      - WOODPECKER_ADMIN=admin
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=http://gitea:3000
      - WOODPECKER_GITEA_CLIENT=supersecret
      - WOODPECKER_GITEA_SECRET=supersecret
      - WOODPECKER_SECRET=woodpeckersecret
    ports:
      - "8000:8000"
    restart: always
    depends_on:
      - gitea

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker-agent
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_SECRET=woodpeckersecret
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    restart: always
    depends_on:
      - woodpecker-server

  harbor:
    image: goharbor/harbor-core:v2.12.0
    container_name: harbor
    environment:
      - HARBOUR_ADMIN_PASSWORD=Harbor12345
    ports:
      - "8081:8080"
    restart: always
    volumes:
      - ./harbor:/data

 

👨‍🍳 Bonus Recipe: Python + PHP CI/CD Pipelines

👉 The goal is simple: check your code (lint), test it (unit tests), then deploy it.

GitLab CI

stages: [lint, test, build, deploy]

lint:python:
  stage: lint
  image: python:3.12
  script:
    - pip install ruff black
    - ruff check .
    - black --check .

lint:php:
  stage: lint
  image: php:8.3-cli
  script:
    - composer install --no-interaction
    - vendor/bin/phpstan analyse
    - vendor/bin/php-cs-fixer fix --dry-run --diff

test:python:
  stage: test
  image: python:3.12
  script:
    - pip install -r requirements.txt
    - pytest -q

test:php:
  stage: test
  image: php:8.3-cli
  script:
    - composer install --no-interaction
    - vendor/bin/phpunit

Woodpecker CI

pipeline:
  lint_python:
    image: python:3.12
    commands:
      - pip install ruff black
      - ruff check .
      - black --check .

  lint_php:
    image: php:8.3-cli
    commands:
      - composer install --no-interaction
      - vendor/bin/phpstan analyse
      - vendor/bin/php-cs-fixer fix --dry-run --diff

  test_python:
    image: python:3.12
    commands:
      - pip install -r requirements.txt
      - pytest -q

  test_php:
    image: php:8.3-cli
    commands:
      - composer install --no-interaction
      - vendor/bin/phpunit

 

🍱 Multi-Stage Dockerfiles

Laravel (PHP)

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.* ./
RUN composer install --no-dev --prefer-dist --no-progress --no-interaction
COPY . .
RUN php artisan route:cache && php artisan config:cache

FROM php:8.3-fpm-alpine
WORKDIR /var/www/html
COPY --from=vendor /app /var/www/html
RUN docker-php-ext-install pdo pdo_mysql opcache
 

FastAPI (Python)

FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml poetry.lock* ./
RUN pip install poetry && poetry export -f requirements.txt -o requirements.txt
RUN pip install -r requirements.txt --target /deps
COPY . .

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.12/site-packages
COPY . .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
 

 

🚚 Automated Deployment with Ansible (Zero Downtime)

Playbook

- hosts: app
  become: true
  tasks:
    - name: Pull nouvelle image
      community.docker.docker_compose:
        project_src: /srv/app/
        pull: yes
        build: no
        state: present
        restarted: yes

    - name: Laravel migrate (si PHP)
      command: docker exec app-php php artisan migrate --force
      when: laravel | default(false)

👉 Run two containers in parallel (app_v1 and app_v2) and let Traefik handle a rolling update for you.

🔒 Air-Gapped Mode (The High-Tech Bunker)

Some companies want CI/CD in a sealed bunker: no internet, no dependencies phoning home to the US, everything stays inside your walls.
Sounds like Dark, but it works.

The secret ingredients

  • Nexus OSS: to mirror PyPI, Composer, npm, Docker Hub.

  • Harbor: to store Docker images internally.

  • Runners: always use internal images.

  • Ansible: deploy to your VMs or cluster without ever calling the outside world.

docker-compose Nexus

version: '3.7'
services:
  nexus:
    image: sonatype/nexus3:latest
    container_name: nexus
    restart: always
    ports:
      - "8082:8081"
    volumes:
      - ./nexus-data:/nexus-data

(Pro tip: set up Nexus in proxy mode → it fetches libraries once, then serves them locally like a master cheesemonger 🧀.)

🎩 Fine Dining Deployment: Deployer PHP for Laravel

Sometimes, you don’t want to bother with Docker Compose, Ansible, or Kubernetes.
You just want your Laravel code to reach the server quickly, cleanly, and with DB migrations executed without sweat.

That’s where Deployer PHP shines.

Imagine: one deploy.php, one dep deploy production, and your Laravel app is served like a Michelin-star dish 🍽️.

  • Pulls code from Git (Gitea, GitLab, whatever).

  • Installs dependencies with Composer.

  • Optimizes Laravel (config:cache, route:cache).

  • Deploys into a versioned releases/ folder.

  • Symlinks current → instant deploy, no downtime.

  • Runs migrations automatically.

👉 Add it to your GitLab or Woodpecker pipeline, and voilà: commit → pipeline → Deployer → production up-to-date.
No sweaty git pull over SSH at 2 AM.

Installation

 

composer require deployer/deployer --dev

Example of deploy.php

<?php
namespace Deployer;

require 'recipe/laravel.php';

// Nom du projet
set('application', 'MonSuperLaravel');

// Repo Git
set('repository', 'git@gitea:moncompte/monrepo.git');

// Branch par défaut
set('branch', 'main');

// Hosts (serveurs cibles)
host('production')
    ->setHostname('monserveur.example.com')
    ->setRemoteUser('deploy')
    ->setPort(22)
    ->set('deploy_path', '/var/www/{{application}}');

// Options Laravel
set('keep_releases', 5);
set('php_fpm_service', 'php8.3-fpm');

// Hooks personnalisés
after('deploy:failed', 'deploy:unlock');

// Tâche de migration auto
after('deploy:symlink', 'artisan:migrate');

Conclusion

➡️ GitLab CE = All-you-can-eat buffet: everything included, but eats RAM for breakfast.
➡️ Gitea + Woodpecker = Gourmet food truck: light, fast, tasty, but needs some assembly.
➡️ Air-Gapped Mode = High-tech bunker: for sensitive industries (defense, health, nuclear).
➡️ Deployer PHP = Fine dining: Laravel deployment as refined as a white-gloved service.

At OBI Partner, we like to say:
“No matter the forge, as long as production comes out hot, without smoke, and without cold pizza.” 🔥

👉 So, are you more of a buffet lover, food truck enjoyer, bunker operator, or fine dining connoisseur?

Add new comment

Restricted HTML

  • You can align images (data-align="center"), but also videos, blockquotes, and so on.
  • You can caption images (data-caption="Text"), but also videos, blockquotes, and so on.