Dagger on a VPS: Container-Native CI/CD Pipelines That Run Anywhere

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.

5 Comments

  • Visit naganotonicleans.com
    October 19, 2026

    This 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, 2026

    Great explanation of why understanding the underlying cause of hair breakage matters.

  • Peeling nails causes
    October 19, 2026

    The article presents nail health information accessibly.

  • GlucoTrust Bottle supplement
    October 19, 2026

    Great insights into factors that may affect hair density.

  • Joint flexibility
    October 19, 2026

    This is a useful resource for learning about hair breakage.

Post Your Comment

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!