Local CI
Local CI is a tool that allows you to run CI/CD pipelines locally using Docker containers. It helps developers test and debug their CI pipelines without pushing to remote repositories.
Features
- Run CI pipeline jobs locally using Docker
- Stages-based execution similar to GitLab CI
- Configuration using YAML format
- Reuse local images by default, with
force_pull for refreshing from the registry
- Private registry credentials at pipeline or job level
- Global and job-specific environment variables
- Working directory customization
- Cache support for dependencies and build artifacts
- Automatic file copying with .gitignore support
- Real-time log streaming from containers
- Automatic container cleanup
- Stage-based pipeline execution
- Job-based pipeline execution
- Parallel job execution (all jobs at once, or per-stage) with a live status board
- Per-job
parallel: true keyword for detaching individual jobs from the sequential chain
- Matrix builds: parametrize a job with
matrix: to fan it out across variable combinations
- Services: sidecar containers (databases, caches) on a private per-job network with readiness gates —
services: [postgres:16]
- Artifacts: pass build outputs from one job to the next with
artifacts: paths: [...]
- DAG scheduling:
needs: [job] starts a job the moment its dependencies pass instead of waiting for the stage barrier
- Per-job
timeout: and retry: for slow or flaky jobs
- Templates and includes: factor common config into
.dot-prefixed templates and pull shared files with include:
- Stage placeholders: splice an included file's stages into the main pipeline at a chosen position
- GitLab utils
- Bootstrap scripts
- Cleanup scripts (companion to bootstrap)
- Per-job bootstrap and cleanup scripts
- Web UI: live pipeline graph, ANSI-colored searchable logs, run control (env vars, per-stage/per-job runs, failed-job re-run), desktop notifications, and a built-in YAML editor with live lint — served from a single binary with
local-ci ui
- Run history with git context (branch + commit per run), plus per-job duration trends and flaky-job detection; inspect from the UI or
local-ci runs / local-ci log
- Debug shell:
local-ci shell <job> drops you into the job's exact container environment — image, variables, workspace, cache mounts, and running services
- Watch mode:
local-ci run --watch re-runs the pipeline on every file change
- Config linting (
local-ci validate) and a .gitlab-ci.yml importer (local-ci import gitlab)
- Claude Code agent plugin
Installation
Prerequisites
- Docker must be installed and running
- Your user must have permissions to access the Docker daemon
- Verify installation with
docker --version
Homebrew (macOS / Linux)
brew install --cask MrPuls/local-ci/local-ci
Shell completions for bash, zsh, and fish are installed automatically.
Go Install
Requires Go 1.26 or later (download). The web UI build is
committed, so the installed binary includes local-ci ui with no extra tooling.
go install github.com/MrPuls/local-ci/cmd/local-ci@latest
Manual
Download the latest binary for your platform (Linux, macOS, or Windows) from the
releases page. The archives also
carry completion scripts (completions/) for bash, zsh, fish, and PowerShell;
or generate them on the fly with local-ci completion <shell>.
Shell completions
Completions are context-aware: local-ci shell <TAB> lists the jobs defined in
your config, -j/-s complete job and stage names, local-ci log <TAB>
completes recent run ids (with their status), and -c offers the discovered
config files. Homebrew installs them for you; otherwise see
local-ci completion --help.
Build from source
A Makefile wraps the common tasks (run make help for the full list). Building
the UI needs Bun; the Go binary needs Go 1.26+.
make build # build the web UI + the local-ci binary into ./bin
make install # build the UI and `go install` the binary
After changing anything under web/, run make web and commit the regenerated
internal/web/dist so the embedded UI stays in sync.
Usage
Check Installation
local-ci --version
Run a Pipeline
# Run using default .local-ci.yaml in current directory
local-ci run
# Run with a specific config file using --config/-c flag
local-ci run --config my-pipeline.yaml
# Run a specific job using --job/-j
local-ci run --job JobName
# Or to run multiple jobs
local-ci run --job jobName1,JobName2
# Run jobs from a specific stage using --stage/-s
local-ci run --stage stageName
# Or use multiple stages
local-ci run --stage stageName1,stageName2
# Run all jobs in parallel with --parallel/-p
local-ci run --parallel
# Or run stages in order, with jobs inside each stage in parallel
local-ci run --parallel-stages
# Clone/update the repository and run it's local-ci.yaml with --remote/-r
local-ci run --remote <repository_url>
# Pass additional environment variables with --env/-e
local-ci run --env NEW_VAR=var_value,SECOND_VAR=new_value
When you don't pass -c/--config, run scans the working directory for config
files — the canonical .local-ci.yaml/.local-ci.yml plus any
<name>.local-ci.yaml / <name>-local-ci.yaml / <name>_local-ci.yaml
variant — and asks which one to load (Enter picks the first). Non-interactive
sessions never block: a single discovered file is used as-is, anything else
falls back to .local-ci.yaml.
Developer loop
# Re-run the pipeline on every file change (Ctrl-C to stop)
local-ci run --watch
# Drop into a job's exact container environment to debug it from the inside:
# same image, variables, workdir, cache mounts, copied workspace — and its
# services running on the job network
local-ci shell Test
# Lint the config without running anything (exit 1 when invalid — git-hook friendly)
local-ci validate
# Convert an existing GitLab CI config (prints notes for anything that can't carry over)
local-ci import gitlab
Web UI
local-ci ui serves the whole web app — its UI and API — from this single
binary, then opens it in your browser. No separate dev server or token to manage.
# Serve the UI for the project in the current directory and open a browser
local-ci ui
# Bind a specific port and don't auto-open a browser
local-ci ui --port 8080 --no-open
# Use a non-default config file
local-ci ui --config my-pipeline.yaml
It binds loopback only (127.0.0.1) and shows the configured pipeline as a live
graph: trigger and cancel runs, watch job status update in real time, stream
logs, and browse run history — all backed by the same engine as local-ci run.
On load the UI scans the project directory for config files (same patterns as
run) and asks which one to drive the session; switch any time via the FILE:
chip in the top bar. The EDITOR tab is a built-in YAML editor with syntax
highlighting, live lint (undefined stages, unknown needs targets, missing
fields — flagged before you save), validation feedback, a diff preview before
discarding edits, and a live pipeline graph — Ctrl+S writes to disk.
The pipeline view exposes the full trigger surface: pick a run mode, set env
vars, run a single stage from its column header, or a single job from the
inspector (failed jobs get a one-click re-run). Logs render ANSI colors and
are searchable. History shows each run's git context (branch@sha) and a
JOB_TRENDS panel: per-job duration sparklines, pass rates, and an
INTERMITTENT flag for jobs that flip between pass and fail. The bell in the
top bar enables desktop notifications when a run finishes in a hidden tab.
For frontend development against a hot-reloading dev server, use
local-ci serve (the API-only backend) together with the Vite dev server —
see web/README.md.
Inspecting past runs
Every run (CLI or UI) is recorded to a local history store.
# List recent runs for the current project (newest first)
local-ci runs
# List runs across all projects, capping the count
local-ci runs --all --limit 50
# Show one run's per-job breakdown
local-ci runs <run-id>
# Print a recorded run's logs — all jobs, or one job
local-ci log <run-id>
local-ci log <run-id> --job Build
local-ci log <run-id> --job pipeline # run-level diagnostics
Command reference
| Command |
Description |
Flags |
run |
Run the pipeline |
-c/--config, -j/--job, -s/--stage, -r/--remote, -e/--env, -p/--parallel, --parallel-stages, -w/--watch, --no-record |
shell <job> |
Open an interactive shell in the job's container environment (services included) |
-c/--config, -v/--verbose |
validate [file] |
Lint a config without running it (exit 1 when invalid) |
-c/--config |
import gitlab [file] |
Convert a .gitlab-ci.yml into a local-ci config |
-o/--output (default .local-ci.yaml, - for stdout), --force |
runs [run-id] |
List recorded runs, or show one run's details |
-a/--all, -n/--limit (default 20) |
log <run-id> |
Print a recorded run's logs |
-j/--job (use pipeline for diagnostics) |
ui |
Serve the embedded web UI and API from one binary, then open a browser |
--host (default 127.0.0.1), --port (default 4123), -c/--config, --no-open |
serve |
Run the API-only backend (for the web dev server or a future desktop shell) |
--host (default 127.0.0.1), --port (default ephemeral), --token (default random), -c/--config |
Global: local-ci --version, local-ci --help, local-ci <command> --help.
Quick Start
-
Start Docker
-
Create a .local-ci.yaml file in your project root:
stages:
- build
- test
variables:
GLOBAL_VAR: global_value
Build:
stage: build
image: golang:1.21
variables:
GO_FLAGS: "-v"
script:
- echo "Building application..."
- go build $GO_FLAGS
cache:
key: go-build-cache
paths:
- .go/
- build/
Test:
stage: test
image: golang:1.21
script:
- echo "Testing with global var: $GLOBAL_VAR"
- go test ./...
cache:
key: go-build-cache
paths:
- .go/
- Run the pipeline:
local-ci run
YAML Configuration
Basic Structure
# Define stages and their order
stages:
- build
- test
- deploy
# Global variables (available to all jobs)
variables:
GLOBAL_KEY: global_value
# Job definitions
job_name:
stage: build # Must match one of the defined stages
image: image_name
workdir: /path # Optional
variables: # Job-specific variables (override globals)
KEY: value
script:
- command1
- command2
cache: # Optional cache configuration
key: cache-key
paths:
- path/to/cache
- another/path
Image Pull Policy
Images are reused from the local Docker daemon when available and pulled only when missing:
image: ubuntu
To always pull from the registry before running the job:
image:
name: ubuntu
force_pull: true
The same image options work for services and interactive shells. force_pull defaults to false.
Private Registries
Private registry credentials can be configured once for all jobs and services:
registries:
registry.example.com:
username: ci-user
password_env: REGISTRY_TOKEN
Set REGISTRY_TOKEN in the environment running local-ci, then use an image such as registry.example.com/team/build:latest. username_env is also supported. For registries without an entry, local-ci uses the existing Docker login for the image's registry. See registry configuration for all options.
You can also declare the registry login and image in the same job:
stages: [build]
Build:
stage: build
registries:
registry.example.com:
username: ci-user
password_env: REGISTRY_TOKEN
image: registry.example.com/team/build:latest
script:
- make build
Job credentials are available before pulling that job's image and services, overriding pipeline credentials for matching hosts. Other jobs keep their own settings. The same configuration works with local-ci shell Build. See job-level registries for precedence and template inheritance.
Claude Code Agent
Local CI ships as a Claude Code plugin. Once installed, Claude Code can autonomously write pipeline configs, run them, and validate your code in real Docker environments.
Install the plugin
/plugin marketplace add MrPuls/local-ci
/plugin install local-ci@MrPuls-local-ci
See the Technical Reference for details.
Documentation
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License