- Posted on
- • Artificial Intelligence
Artificial Intelligence GitLab CI Pipelines
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence GitLab CI Pipelines: From Notebook to Repeatable Linux Automation
If you’ve ever heard “it works on my machine” right before a model demo fails, you’ve already met the core problem AI teams face: reproducibility. AI stacks are dependency-heavy, data-hungry, and GPU-sensitive. The fix is boring—in the best way possible: codify the workflow with GitLab CI so you can go from notebook to repeatable pipeline on any Linux box or runner.
This article shows you how to stand up a robust, Bash-friendly GitLab CI pipeline for AI projects, with practical YAML and shell snippets, GPU notes, and distro-specific install steps for apt, dnf, and zypper.
Why GitLab CI for AI?
Repeatability: Pin environments, cache models, and make training deterministic.
Speed: CI caches and parallel stages slash iteration time.
Traceability: Every run is logged, versioned, and easy to roll back.
Scale: Add runners (CPU/GPU) as your workloads grow.
Governance: Secrets, approvals, and audit trails are first-class citizens.
What you’ll build
A CI pipeline that lints and tests Python, then trains a model (CPU or GPU), and packages artifacts.
Smart caching for Python deps and model weights.
Optional GPU acceleration via NVIDIA Container Toolkit.
Bash-first, minimal tooling, distro-agnostic instructions.
Prerequisites and installation
You need:
A Linux machine or VM with sudo
A GitLab project (SaaS or self-managed)
Docker Engine (or Podman for basic CPU jobs)
Optional: An NVIDIA GPU for accelerated training
Base tools
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y git curl python3 python3-venv python3-pip jq
Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y git curl python3 python3-venv python3-pip jq
openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y git curl python3 python3-venv python3-pip jq
Container runtime (Docker or Podman)
Docker Engine
- apt:
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
- dnf:
sudo dnf install -y moby-engine
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
- zypper:
sudo zypper install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
Podman (CPU-only pipelines without Docker-in-Docker)
- apt:
sudo apt update
sudo apt install -y podman
- dnf:
sudo dnf install -y podman
- zypper:
sudo zypper install -y podman
GitLab Runner
- apt:
curl -fsSL https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt install -y gitlab-runner
- dnf:
curl -fsSL https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
sudo dnf install -y gitlab-runner
sudo systemctl enable --now gitlab-runner
- zypper:
curl -fsSL https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
sudo zypper install -y gitlab-runner
sudo systemctl enable --now gitlab-runner
Register your runner (Docker executor is the most flexible):
sudo gitlab-runner register
# URL: https://gitlab.com/ or your self-managed URL
# Registration token: from your project/group
# Description: ai-docker-runner
# Tags: cpu,linux (add gpu later if needed)
# Executor: docker
# Default image: python:3.11-slim
Optional persistent cache volume (improves speed):
- Edit /etc/gitlab-runner/config.toml and add to your runner’s docker config:
volumes = ["/cache"]
- Restart:
sudo systemctl restart gitlab-runner
Optional: NVIDIA GPU support for containers
Install NVIDIA Container Toolkit so Docker can pass the GPU into containers.
- apt:
distribution=$(. /etc/os-release; echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
- dnf:
distribution=$(. /etc/os-release; echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo \
| sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
sudo dnf install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
- zypper:
distribution=$(. /etc/os-release; echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo \
| sudo tee /etc/zypp/repos.d/nvidia-container-toolkit.repo
sudo zypper refresh
sudo zypper install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
Give your GitLab Runner access to GPUs by setting gpus = "all":
- Edit /etc/gitlab-runner/config.toml for a “gpu” runner:
[[runners]]
name = "gpu-docker"
executor = "docker"
tags = ["gpu","linux"]
[runners.docker]
image = "pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime"
gpus = "all"
volumes = ["/cache"]
- Restart:
sudo systemctl restart gitlab-runner
Repository layout (minimal)
.
├── .gitlab-ci.yml
├── requirements.txt
├── train.py
└── inference.py
requirements.txt:
numpy
scikit-learn
joblib
train.py (CPU by default; uses GPU if PyTorch+cudevices are present, e.g., in the GPU job image):
#!/usr/bin/env python3
import os, json, time, pathlib
from pathlib import Path
def save_meta(path, meta: dict):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(meta, indent=2))
def cpu_train():
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import joblib
X, y = load_iris(return_X_y=True, as_frame=False)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)
m = LogisticRegression(max_iter=200)
m.fit(Xtr, ytr)
acc = m.score(Xte, yte)
Path("artifacts").mkdir(exist_ok=True)
joblib.dump(m, "artifacts/model.joblib")
save_meta(Path("artifacts/metrics.json"), {"accuracy": acc, "device": "cpu"})
print(f"CPU model accuracy: {acc:.4f}")
def gpu_train():
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
x = torch.randn(1024, 512, device=device)
w = torch.randn(512, 256, device=device, requires_grad=True)
y = x @ w
loss = y.pow(2).mean()
loss.backward()
torch.save({"W": w.detach().cpu()}, "artifacts/model.pt")
save_meta(Path("artifacts/metrics.json"), {"loss": float(loss.item()), "device": device})
print(f"GPU path ran on: {device}, loss={loss.item():.6f}")
def main():
t0 = time.time()
Path("artifacts").mkdir(exist_ok=True)
try:
import torch # noqa
gpu_train()
except Exception as e:
print(f"Falling back to CPU training: {e}")
cpu_train()
print(f"Done in {time.time() - t0:.2f}s")
if __name__ == "__main__":
main()
inference.py (toy CLI for packaged model):
#!/usr/bin/env python3
import sys, joblib, json
from pathlib import Path
m = joblib.load("artifacts/model.joblib")
print(json.dumps({"pred": m.predict([[float(v) for v in sys.argv[1:5]]]).tolist()}))
The GitLab CI pipeline (.gitlab-ci.yml)
This pipeline:
Caches Python and model caches between jobs.
Lints/tests quickly in a slim image.
Trains on CPU by default and optionally on GPU via a separate job.
Publishes model artifacts for later promotion.
stages:
- verify
- train
- package
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
HF_HOME: "$CI_PROJECT_DIR/.cache/huggingface"
PIP_NO_INPUT: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PYTHONDONTWRITEBYTECODE: "1"
cache:
key: "pip-${CI_COMMIT_REF_SLUG}"
paths:
- .cache/pip
- .cache/huggingface
policy: pull-push
verify:
stage: verify
image: python:3.11-slim
before_script:
- python -V
- python -m pip install --upgrade pip
- pip install flake8 pytest -r requirements.txt
script:
- flake8 .
- pytest -q || true # optional if no tests yet
rules:
- if: $CI_COMMIT_BRANCH
train:cpu:
stage: train
image: python:3.11-slim
needs: ["verify"]
before_script:
- python -m pip install --upgrade pip
- pip install -r requirements.txt
script:
- python train.py
artifacts:
when: always
expire_in: 7 days
paths:
- artifacts/
rules:
- if: $CI_COMMIT_BRANCH
tags:
- cpu
# Optional GPU job; requires a runner tagged "gpu" and NVIDIA toolkit
train:gpu:
stage: train
image: pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime
needs: ["verify"]
before_script:
- pip install -r requirements.txt
script:
- python train.py
- python -c "import torch; print('CUDA?', torch.cuda.is_available())"
artifacts:
when: always
expire_in: 7 days
paths:
- artifacts/
rules:
- if: '$ENABLE_GPU == "1"'
tags:
- gpu
# Create a signed tarball of artifacts for promotion
package:
stage: package
image: bash:latest
needs: ["train:cpu"]
script:
- mkdir -p dist
- tar czf dist/model-${CI_COMMIT_SHORT_SHA}.tar.gz artifacts
- sha256sum dist/model-${CI_COMMIT_SHORT_SHA}.tar.gz | tee dist/SHA256SUMS
artifacts:
expire_in: 14 days
paths:
- dist/
rules:
- if: $CI_COMMIT_TAG
Notes:
Enable GPU path per pipeline by setting CI/CD variable ENABLE_GPU=1.
For private model hubs, store tokens as masked variables (e.g., HF_TOKEN) and let your training scripts read from os.environ without echoing secrets.
4 actionable practices that pay off immediately
1) Make pipelines hermetic and cache aggressively
Pin images (python:3.11-slim, pytorch with a specific CUDA tag).
Cache Python wheels and model weights:
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
HF_HOME: "$CI_PROJECT_DIR/.cache/huggingface"
cache:
paths: [.cache/pip, .cache/huggingface]
- In Bash scripts, fail fast for reliability:
set -euo pipefail
IFS=$'\n\t'
2) Split fast feedback from heavy compute
Keep verify jobs <2 minutes by using slim images and avoiding heavyweight installs.
Run train jobs on tagged runners (cpu, gpu) so you don’t starve basic CI tasks.
3) Treat models as artifacts
- Always save model files and a small metrics.json:
python - <<'PY'
import json, os, pathlib
pathlib.Path("artifacts").mkdir(exist_ok=True)
json.dump({"git_sha": os.environ.get("CI_COMMIT_SHA")}, open("artifacts/build_meta.json","w"))
PY
- Use artifacts to promote models through environments without rebuilding.
4) Secure your AI supply chain
Store tokens (HF_TOKEN, WANDB_API_KEY) as protected, masked variables.
Prefer non-root containers and drop capabilities by default.
Consider dependency proxy and private PyPI to speed and control package sources.
Optional: containerize your inference
If you want a tiny image for inference (CPU), add a Dockerfile and build on tags. This assumes a runner with Docker available.
Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY artifacts/ artifacts/
RUN pip install --no-cache-dir joblib scikit-learn
COPY inference.py .
ENTRYPOINT ["python","inference.py"]
CI job (add to .gitlab-ci.yml):
containerize:
stage: package
image: docker:26
services: ["docker:26-dind"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
needs: ["train:cpu"]
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- docker build -t "$CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | tr / -)" .
- docker push "$CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | tr / -)"
rules:
- if: $CI_COMMIT_TAG
Real-world tip: warm up the GPU runner
First job on a fresh GPU runner can be slow due to cold caches. Add a short warm-up step to your GPU job:
python - <<'PY'
import torch
x = torch.randn(4096,4096, device='cuda')
y = x @ x
torch.cuda.synchronize()
print("Warmed up CUDA.")
PY
Conclusion and next step (CTA)
AI is fragile until you script it. With a GitLab CI pipeline, you make your model lifecycle boringly reliable: lint, test, train, and package on any Linux runner—with or without GPUs.
Your next steps: 1) Install Docker and GitLab Runner (apt/dnf/zypper commands above). 2) Register a CPU runner; optionally add a GPU runner with NVIDIA toolkit. 3) Drop the provided .gitlab-ci.yml, train.py, and requirements.txt into your repo. 4) Push a branch, watch the pipeline go green, then tag a release to package.
When you’re ready, extend this with dataset versioning (DVC), experiment tracking (MLflow/W&B), and container scanning. If you want a follow-up post on multi-arch images, spot/preemptible GPU runners, or Hugging Face cache best practices, say the word.