Dagger on a VPS: Container-Native CI/CD Pipelines That Run Anywhere
Dagger is a CI/CD engine that runs your build and test pipelines as directed acyclic graphs of containers — identical behavior locally on your laptop, in Gitea Actions, GitHub Actions, or on a VPS. You write pipelines in Python, Go, TypeScript, or PHP (using the Dagger SDK); those pipelines run in Docker containers with layer caching. No more “it works in CI but not locally” — the same containerized pipeline runs everywhere.
Why Dagger Changes CI/CD
- Local = CI: Debug failing pipelines locally in seconds instead of push-and-wait cycles
- Caching: Dagger caches container layers across runs — only changed steps re-run
- Language-native: Pipelines in Python/Go — IDE autocomplete, type checking, testing for your pipeline code
- Portable: Same pipeline file runs on Gitea, GitHub, GitLab, or any machine with Docker
- Composable: Share pipeline modules across repositories (Daggerverse)
Step 1: Install Dagger
<code"># Install Dagger CLI curl -L https://dl.dagger.io/dagger/install.sh | sh # Verify dagger version # Dagger requires Docker: sudo apt install -y docker.io sudo usermod -aG docker $USER
Step 2: Create a Dagger Pipeline in Python
<code">mkdir -p /opt/myapp-pipelines && cd /opt/myapp-pipelines pip install dagger-io nano pipeline.py
<code">"""
Dagger pipeline for building and testing a Node.js app.
Run locally: python pipeline.py
Run in CI: same command
"""
import sys
import anyio
import dagger
async def main():
config = dagger.Config(log_output=sys.stderr)
async with dagger.Connection(config) as client:
# Get source code from current directory
src = client.host().directory(".")
# Build and test pipeline as a graph of containers
node = (
client.container()
.from_("node:20-alpine")
# Cache node_modules across builds (key = package.json content)
.with_mounted_cache(
"/app/node_modules",
client.cache_volume("node-modules"),
)
.with_directory("/app", src)
.with_workdir("/app")
# Install dependencies (cached if package.json unchanged)
.with_exec(["npm", "ci"])
)
# Run tests
print("Running tests...")
test_result = await (
node
.with_exec(["npm", "test"])
.stdout()
)
print(test_result)
# Build production bundle
print("Building...")
build = await (
node
.with_exec(["npm", "run", "build"])
)
# Build Docker image
print("Building Docker image...")
image = (
client.container()
.from_("node:20-alpine")
.with_directory("/app", build.directory("/app/dist"))
.with_workdir("/app")
.with_exec(["npm", "ci", "--production"])
.with_entrypoint(["node", "index.js"])
)
# Push to registry (if on main branch)
image_ref = await image.publish(
"git.yourdomain.com/myorg/myapp:latest"
)
print(f"Published: {image_ref}")
anyio.run(main)
<code"># Run locally: python pipeline.py # Run in CI (identical command): dagger run python pipeline.py
Step 3: Integrate with Gitea Actions
<code"># .gitea/workflows/ci.yml
name: CI with Dagger
on:
push:
branches: [main, develop]
pull_request:
jobs:
dagger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Dagger SDK
run: pip install dagger-io
- name: Run Dagger Pipeline
env:
REGISTRY_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: python pipeline.py
Step 4: Multi-Stage Pipeline with Parallel Execution
<code">nano full-pipeline.py
<code">import anyio
import dagger
async def test(client, src):
"""Run tests in parallel: unit and lint simultaneously."""
node = (
client.container()
.from_("node:20-alpine")
.with_mounted_cache("/app/node_modules", client.cache_volume("node-modules"))
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["npm", "ci"])
)
# Run unit tests and linting in PARALLEL (both start simultaneously)
unit_tests, lint = await anyio.create_task_group()
async def run_unit():
return await node.with_exec(["npm", "test"]).stdout()
async def run_lint():
return await node.with_exec(["npm", "run", "lint"]).stdout()
# Both run concurrently in separate containers
results = await anyio.gather(run_unit(), run_lint())
return results
async def security_scan(client, src):
"""Semgrep security scan."""
return await (
client.container()
.from_("semgrep/semgrep:latest")
.with_directory("/src", src)
.with_exec([
"semgrep", "--config", "p/owasp-top-ten",
"--error", "--json", "/src"
])
.stdout()
)
async def build_and_push(client, src):
"""Build Docker image and push to registry."""
image = (
client.container()
.from_("node:20-alpine")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["npm", "ci", "--production"])
)
return await image.publish("git.yourdomain.com/myorg/myapp:latest")
async def main():
async with dagger.Connection() as client:
src = client.host().directory(".", exclude=[".git", "node_modules"])
# Stage 1: Test + Security (parallel)
test_results, security_results = await anyio.gather(
test(client, src),
security_scan(client, src),
)
# Stage 2: Build and push (only after both pass)
image_ref = await build_and_push(client, src)
print(f"Deployed: {image_ref}")
anyio.run(main)
Step 5: Dagger Modules (Reusable Pipeline Components)
<code"># Dagger modules are shareable, versioned pipeline components # Install a community module: dagger install github.com/shykes/daggerverse/[email protected] # Use in your pipeline: import dagger from dagger import dag # Use the Docker module result = await dag.docker().build( context=dag.host().directory("."), dockerfile="Dockerfile", ) # Share your own module on the Daggerverse
Getting Started
Dagger runs anywhere Docker runs — on your laptop for local development and on any Ubuntu VPS at VPS.DO acting as a CI runner. The container caching means subsequent builds of a Node.js app skip npm install if package.json hasn’t changed, reducing build times from 3 minutes to 30 seconds. Python/Go pipeline code is testable — write unit tests for your pipeline steps the same way you test application code.
Conclusion
Dagger eliminates the CI environment debugging cycle — containerized pipelines run identically locally and in CI, with aggressive layer caching reducing build times. Python and Go pipeline code benefits from IDE support, type checking, and unit testing. For teams tired of pushing to CI to test pipeline changes, Dagger’s “run it exactly as CI runs it” approach is transformative. The growing Daggerverse module ecosystem provides pre-built pipeline components for Docker, Helm, Terraform, and common language toolchains.
Visit naganotonicleans.com
October 19, 2026This is a well-presented introduction to brain health. It would be interesting to learn more about evidence-based ways to support memory and concentration.
Choline-rich foods for brain health
October 19, 2026Great explanation of why understanding the underlying cause of hair breakage matters.
Peeling nails causes
October 19, 2026The article presents nail health information accessibly.
GlucoTrust Bottle supplement
October 19, 2026Great insights into factors that may affect hair density.
Joint flexibility
October 19, 2026This is a useful resource for learning about hair breakage.