A cloud-agnostic build and deployment CLI. Define your service once, deploy to any cloud provider.
pilum deploy --tag=v1.0.0Pilum handles the build → push → deploy pipeline while your infrastructure-as-code (Terraform, Pulumi) defines the actual resources.
- Recipe-driven deployments - Define reusable deployment workflows in YAML
- Recipe-driven validation - Each recipe declares required fields, no Go code per provider
- Multi-cloud support - GCP Cloud Run, AWS Lambda, Azure Container Apps, Homebrew, and more
- Parallel execution - Deploy multiple services concurrently with step barriers
- Dependency ordering - Wave-based deployment respects
depends_onacross services - Deployment locks - Prevent concurrent deploys with automatic stale lock cleanup
- GitHub commit status - Post pending/success/failure status to commits from CI
- Step filtering - Run only build steps, only deploy steps, or custom tag combinations
- Environment overrides - Per-environment config (staging, prod) via
environmentsblock - Git-aware filtering - Deploy only services with changes since base branch
- Dry-run mode - Preview commands before executing
- JSON output - Machine-readable output for scripting and AI agents
- Editor support - JSON Schema for autocompletion and validation in VS Code, JetBrains, etc.
- Beautiful CLI - Animated spinners, colored output, clear progress
brew tap sid-technologies/pilum
brew install pilumgo install github.com/sid-technologies/pilum@latestUse the interactive init command to generate a pilum.yaml:
pilum initThis walks you through:
- Selecting a provider (GCP, AWS, Homebrew, etc.)
- Selecting a service type (Cloud Run, Lambda, etc.)
- Filling in required and optional fields
- Choosing a build language (Go, Python, Rust, Node)
Create a pilum.yaml in your project:
# yaml-language-server: $schema=https://raw.githubusercontent.com/SID-Technologies/pilum/main/schemas/pilum.yaml.schema.json
name: my-api
provider: gcp
project: my-gcp-project
region: us-central1
registry_name: gcr.io/my-gcp-project
build:
language: go
version: "1.23"pilum checkThis validates your pilum.yaml against the recipe's required fields.
# Deploy all services
pilum deploy --tag=v1.0.0
# Deploy specific service
pilum deploy my-api --tag=v1.0.0
# Preview without executing
pilum deploy --dry-run --tag=v1.0.0
# Build and push only (no deploy)
pilum publish --tag=v1.0.0Pilum uses a cooking metaphor: recipes define deployment workflows, ingredients generate cloud-specific commands.
Each recipe defines:
- Steps - Ordered commands to execute (build, push, deploy)
- Required fields - What your
pilum.yamlmust contain - Optional fields - Additional configuration options with defaults
| Recipe | Provider | Description |
|---|---|---|
gcp-cloud-run |
gcp |
Deploy to Google Cloud Run |
gcp-cloud-run-job |
gcp |
Deploy batch jobs to Google Cloud Run Jobs |
aws-lambda |
aws |
Deploy to AWS Lambda |
azure-container-apps |
azure |
Deploy to Azure Container Apps |
cloudflare-pages |
cloudflare |
Deploy to Cloudflare Pages |
homebrew |
homebrew |
Publish Homebrew formula |
npm |
npm |
Publish npm packages |
Use pilum recipes to list all recipes, or pilum recipes <name> to see required fields, optional fields, and deployment steps for a specific recipe.
Create your own recipes in recipes/:
name: my-recipe
description: My deployment workflow
provider: my-provider
service: my-service # Required - service type identifier
required_fields:
- name: cluster
description: Kubernetes cluster name
type: string
- name: namespace
description: Target namespace
type: string
default: default # Optional default
optional_fields:
- name: replicas
description: Number of replicas
type: int
default: "1"
steps:
- name: build
command: go build -o dist/app .
execution_mode: root
timeout: 300
- name: deploy
command: kubectl apply -f k8s/
execution_mode: root
timeout: 60See recipes/README.md for full documentation.
Pilum prevents concurrent deployments using a lock file at .pilum/deploy.lock. If you run pilum deploy while another deploy is in progress, the second invocation will fail with details about who holds the lock.
# Fails if another deploy is running
pilum deploy --tag=v1.0.0
# ✗ deployment locked by PID 12345 on my-host (started 2026-02-13T10:00:00Z, command: deploy)
# Override the lock
pilum deploy --tag=v1.0.0 --forceLocks are automatically cleaned up when:
- The holding process has exited (dead PID detection)
- The lock is older than 1 hour (stale timeout)
Dry-run mode does not acquire locks.
Post deployment status to GitHub commits directly from your CI pipeline:
pilum deploy --tag=v1.0.0 --github-statusThis posts pending before the run, then success or failure after, using the pilum/<command> context (e.g., pilum/deploy).
Required environment variables (set automatically in GitHub Actions):
GITHUB_ACTIONS=trueGITHUB_TOKEN- GitHub token withrepo:statuspermissionGITHUB_SHA- Commit SHA to updateGITHUB_REPOSITORY- Repository inowner/repoformat
- name: Deploy
run: pilum deploy --tag=${{ github.sha }} --github-status
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Define per-environment configuration that merges with your base config:
name: api
provider: gcp
project: acme-dev
region: us-central1
environments:
staging:
project: acme-staging
prod:
project: acme-prod
region: us-east1
cloud_run:
min_instances: 2# Deploy with prod overrides
pilum deploy --env=prod --tag=v1.0.0Scalar values are replaced, maps are deep-merged, arrays are replaced entirely.
Services can declare dependencies. Deploy steps execute in waves - dependencies deploy first:
# db/pilum.yaml
name: db-migration
provider: gcp
# api/pilum.yaml
name: api
provider: gcp
depends_on:
- db-migrationWave 1: db-migration ✓ (3.2s)
Wave 2: api ✓ (2.9s)
Build steps always run in flat parallel (no wave ordering). Use --no-deps to disable.
Pilum provides a JSON Schema for pilum.yaml that enables autocompletion, validation, and inline documentation in supported editors.
Add this comment to the top of your pilum.yaml:
# yaml-language-server: $schema=https://raw.githubusercontent.com/SID-Technologies/pilum/main/schemas/pilum.yaml.schema.json
name: my-service
provider: gcpMap the schema URL to pilum.yaml files in Settings > Languages & Frameworks > Schemas and DTDs > JSON Schema Mappings.
| Command | Alias | Description |
|---|---|---|
pilum deploy [services...] |
up |
Full pipeline: build, push, and deploy |
pilum build [services...] |
b, make |
Build step only |
pilum publish [services...] |
p |
Build and push (no deploy) |
pilum push [services...] |
ps |
Push images to registry only |
| Command | Alias | Description |
|---|---|---|
pilum init |
Generate a new pilum.yaml interactively |
|
pilum check [services...] |
validate |
Validate configs against recipe requirements |
pilum list |
ls |
List all discovered services |
pilum recipes [name] |
providers |
List recipes or describe a specific one |
pilum dry-run [services...] |
dr |
Preview commands without executing |
| Command | Alias | Description |
|---|---|---|
pilum status [services...] |
st |
Show status of deployed services |
pilum logs <service> |
log |
Show logs for a deployed service |
pilum history |
hist |
Show deployment history |
| Command | Alias | Description |
|---|---|---|
pilum delete-builds [services...] |
clean |
Delete dist/ directories |
pilum completion |
Generate shell completion script |
| Flag | Short | Description |
|---|---|---|
--verbose |
-v |
Stream command output in real-time |
--quiet |
-q |
Minimal output (CI-friendly) |
--json |
Output as JSON for scripting | |
--no-gitignore |
Don't read .gitignore for ignore patterns |
| Flag | Short | Default | Description |
|---|---|---|---|
--tag |
-t |
latest |
Version tag for the deployment |
--dry-run |
-D |
false |
Preview commands without executing |
--debug |
-d |
false |
Enable debug logging |
--timeout |
-T |
60 |
Command timeout in seconds |
--retries |
-r |
3 |
Number of retries on failure |
--max-workers |
0 (auto) |
Maximum parallel workers | |
--only-tags |
Only run steps with these tags | ||
--exclude-tags |
Exclude steps with these tags | ||
--env |
-e |
Environment to apply (merges overrides) | |
--provider |
Filter services by provider | ||
--only-changed |
false |
Only deploy services with changes since base branch | |
--since |
Git ref to compare against (default: main or master) | ||
--no-deps |
false |
Disable dependency-based deployment ordering | |
--force |
-f |
false |
Override deployment lock |
--github-status |
false |
Post commit status to GitHub |
# Initialize a new service (interactive)
pilum init
# Validate all service configurations
pilum check
# Explore available recipes and their fields
pilum recipes
pilum recipes gcp-cloud-run
# Deploy all services
pilum deploy --tag=v1.0.0
# Deploy specific service
pilum deploy my-api --tag=v1.0.0
# Deploy with environment overrides
pilum deploy --env=prod --tag=v1.0.0
# Deploy only changed services
pilum deploy --only-changed --tag=v1.0.0
# Deploy only GCP services
pilum deploy --provider=gcp --tag=v1.0.0
# Build only (skip deployment)
pilum build --tag=v1.0.0
# Build and push, but don't deploy
pilum publish --tag=v1.0.0
# Run only deploy-tagged steps (assumes images exist)
pilum deploy --only-tags=deploy --tag=v1.0.0
# Override an existing deployment lock
pilum deploy --tag=v1.0.0 --force
# Post GitHub commit status from CI
pilum deploy --tag=v1.0.0 --github-status
# Preview what would run
pilum dry-run --tag=v1.0.0
# Check status of deployed services
pilum status
pilum status my-api
# Stream logs
pilum logs my-api --follow
# View deployment history
pilum history --limit=20
# JSON output (for scripting / AI agents)
pilum list --json
pilum recipes gcp-cloud-run --json
pilum status --jsonmy-project/
├── .pilum/ # State directory (gitignored)
│ ├── history.jsonl # Deployment history
│ └── deploy.lock # Deployment lock (temporary)
├── _templates/ # Dockerfile templates
├── recipes/ # Recipe definitions (YAML)
│ ├── gcp-cloud-run-recipe.yaml
│ └── aws-lambda-recipe.yaml
├── schemas/ # JSON Schema for editor support
│ └── pilum.yaml.schema.json
├── services/
│ ├── api/
│ │ ├── pilum.yaml
│ │ └── main.go
│ └── worker/
│ ├── pilum.yaml
│ └── main.go
└── .pilumignore # Additional ignore patterns
- Discovery - Pilum finds all
pilum.yamlfiles in your project (respects.gitignoreand.pilumignore) - Validation - Each service is validated against its recipe's required fields
- Matching - Services are matched to recipes based on
providerfield - Locking - A deployment lock prevents concurrent runs
- Orchestration - Steps execute in order; services run in parallel within steps
- Recording - Results are saved to
.pilum/history.jsonl
Step 1: build
├── api-gateway ✓ (1.3s)
├── user-service ✓ (1.2s)
└── payment-service ✓ (1.1s)
Step 2: push
├── api-gateway ✓ (2.1s)
├── user-service ✓ (1.8s)
└── payment-service ✓ (2.0s)
Step 3: deploy (wave-ordered)
Wave 1: database ✓ (3.2s)
Wave 2: api-gateway ✓ (2.9s)
user-service ✓ (3.1s)
| Component | Purpose |
|---|---|
cmd/ |
CLI commands (Cobra) |
lib/recipe/ |
Recipe loading and validation |
lib/registry/ |
Step handler registration |
lib/orchestrator/ |
Parallel execution engine |
lib/service_info/ |
Service discovery and filtering |
lib/lock/ |
Deployment lock management |
lib/ci/ |
CI integrations (GitHub commit status) |
lib/history/ |
Deployment history recording |
lib/git/ |
Git-aware change detection |
ingredients/ |
Cloud-specific command generators |
recipes/ |
Deployment workflow definitions |
schemas/ |
JSON Schema for editor support |
Cause: the Docker build daemon can't find a path the Dockerfile is trying to COPY — even though it exists on disk. The usual culprit is .dockerignore filtering the path out of the build context before it reaches the daemon.
Pilum's Go pipeline compiles binaries into services/<name>/dist/ in the build step, then the golang-api Dockerfile does COPY ./services/<name>/dist . in the image step. If your repo's .dockerignore contains **/dist, dist/, or any pattern that matches those directories, they're stripped from the context and the COPY fails with:
ERROR: failed to compute cache key: failed to calculate checksum of ref …::…: "/services/<name>/dist": not found
Docker also emits (easy to miss) a warning line:
1 warning found:
- CopyIgnoredFile: Attempting to Copy file "services/<name>/dist" that is excluded by .dockerignore (line N)
Fix: remove the dist pattern from .dockerignore, or narrow it to only frontend build outputs (e.g., apps/web/dist, apps/desktop/dist) rather than the repo-wide **/dist.
Mental model. .gitignore controls what git tracks. .dockerignore controls what Docker ships to the daemon at build time — files can exist on disk but be invisible to the build. If a Dockerfile COPYs a path, that path MUST NOT match anything in .dockerignore, even if it's a build artifact. Two-strategy repos (one flow compiles inside the image, another compiles outside) need to be careful — the dist you ignore for one strategy is the dist the other needs.
Full documentation available at pilum.dev/docs
- Getting Started - Introduction and quick start
- Service Configuration - Full
pilum.yamlreference - Build Config - Language-specific build settings
- Multi-Service Monorepo - Managing multiple services
- How Recipes Work - Recipe system deep dive
- CLI Commands - Complete CLI reference
- Adding a Provider - Extend Pilum with new providers
- Troubleshooting - Common issues and solutions
Contributions are welcome! See CONTRIBUTING.md for:
- Development setup
- Code style guidelines
- How to add new providers
- Pull request process