<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Francesc Gil</title>
    <description>The latest articles on DEV Community by Francesc Gil (@xescugc).</description>
    <link>https://dev.to/xescugc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957091%2Fac26491c-000c-4961-baba-e282dfdd4cfe.png</url>
      <title>DEV Community: Francesc Gil</title>
      <link>https://dev.to/xescugc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xescugc"/>
    <language>en</language>
    <item>
      <title>PikoCI v0.5.0: worker tagging, in_parallel, Linux packages, and more</title>
      <dc:creator>Francesc Gil</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:14:39 +0000</pubDate>
      <link>https://dev.to/xescugc/pikoci-v050-worker-tagging-inparallel-linux-packages-and-more-32k0</link>
      <guid>https://dev.to/xescugc/pikoci-v050-worker-tagging-inparallel-linux-packages-and-more-32k0</guid>
      <description>&lt;p&gt;Six months into building PikoCI and v0.5.0 is the biggest release yet. This one is almost entirely about workers: routing work to the right place, running things in parallel, and knowing what your workers are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker tagging
&lt;/h2&gt;

&lt;p&gt;You can now route jobs and resource checks to specific workers using tags. Add &lt;code&gt;tags = ["gpu"]&lt;/code&gt; to a job in your pipeline HCL, start a worker with &lt;code&gt;--tags gpu&lt;/code&gt;, and that job will only run on workers with the matching tag. Matching uses AND logic: all job tags must be present on the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"train-model"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"gpu"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"train"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python"&lt;/span&gt;
      &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"train.py"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers can also be made exclusive. The &lt;code&gt;--exclusive-tags&lt;/code&gt; flag restricts a worker to only handle tagged work, keeping it free for the jobs that need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker health monitoring
&lt;/h2&gt;

&lt;p&gt;Workers now send periodic heartbeats to the server. A new admin dashboard shows all connected workers with status, platform, version, and uptime. If a worker goes stale (no heartbeat for 90 seconds), a warning appears and admins can remove it from the UI or via the API.&lt;/p&gt;

&lt;p&gt;There is also a &lt;code&gt;pikoci_workers&lt;/code&gt; Prometheus gauge with a &lt;code&gt;status&lt;/code&gt; label if you want to alert on missing workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  in_parallel step
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcx3z66431xirzghktlv5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcx3z66431xirzghktlv5.png" alt=" " width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run multiple steps concurrently within a job. Useful for running tests against multiple configurations simultaneously, or for parallelizing independent tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;in_parallel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;limit&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="nx"&gt;fail_fast&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"test-postgres"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"make"&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"test-postgres"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"test-mysql"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"make"&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"test-mysql"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"test-sqlite"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"make"&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"test-sqlite"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;limit&lt;/code&gt; caps how many run simultaneously. &lt;code&gt;fail_fast&lt;/code&gt; cancels remaining steps on the first failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  pipeline validate
&lt;/h2&gt;

&lt;p&gt;Validate a pipeline HCL file without a running server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pikoci pipeline validate pipeline.hcl
pikoci pipeline validate pipeline.hcl &lt;span class="nt"&gt;--var&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful for pre-commit hooks and CI checks on the pipeline config itself. Catches syntax errors, structural errors, and invalid cross-references, all reported at once with line numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux packages
&lt;/h2&gt;

&lt;p&gt;PikoCI now ships as &lt;code&gt;.deb&lt;/code&gt; and &lt;code&gt;.rpm&lt;/code&gt; packages alongside the existing binaries. Both amd64 and arm64. A SHA256SUMS file is included in every release.&lt;/p&gt;

&lt;h2&gt;
  
  
  fs resource type
&lt;/h2&gt;

&lt;p&gt;A built-in resource type for watching local files and directories. Triggers jobs when file contents change via SHA256 hash comparison.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="s2"&gt;"fs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://fs"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"fs"&lt;/span&gt; &lt;span class="s2"&gt;"config"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/etc/myapp/config.yaml"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful for local development workflows and watching config files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker communication now uses gRPC
&lt;/h2&gt;

&lt;p&gt;Workers connect to the server via a persistent gRPC stream instead of the previous queue-based system. Job dispatch is instant, cancellation signals reach the worker in milliseconds instead of waiting for a polling interval, and log streaming happens on the same connection. No external queue needed for distributed workers.&lt;/p&gt;

&lt;p&gt;A dedicated post covering the full migration from queues to gRPC is coming soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other fixes
&lt;/h2&gt;

&lt;p&gt;New jobs no longer replay the entire version backlog on creation: they record a baseline at creation time and only see newer versions. Downstream jobs now trigger immediately when upstream builds succeed, instead of waiting for the next scheduler tick. Cross-reference validation catches invalid references at save time with suggestions for typos.&lt;/p&gt;




&lt;p&gt;&lt;a href="//github.com/pikoci/pikoci"&gt;github.com/pikoci/pikoci&lt;/a&gt; · &lt;a href="//pikoci.com"&gt;pikoci.com&lt;/a&gt; · Apache 2.0&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/PikoCI/pikoci/releases/tag/v0.5.0" rel="noopener noreferrer"&gt;v0.5.0 release notes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devop</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Services without Docker-in-Docker: how PikoCI handles test dependencies</title>
      <dc:creator>Francesc Gil</dc:creator>
      <pubDate>Tue, 09 Jun 2026 08:13:24 +0000</pubDate>
      <link>https://dev.to/xescugc/services-without-docker-in-docker-how-pikoci-handles-test-dependencies-5c74</link>
      <guid>https://dev.to/xescugc/services-without-docker-in-docker-how-pikoci-handles-test-dependencies-5c74</guid>
      <description>&lt;p&gt;If you've ever needed a database for your integration tests in CI, you've probably encountered one of these solutions:&lt;/p&gt;

&lt;p&gt;Docker-in-Docker. A separate docker-compose file you run alongside CI. A pre-provisioned shared database that everyone fights over. Or just skipping integration tests in CI entirely.&lt;/p&gt;

&lt;p&gt;None of these are great. Docker-in-Docker requires privileged containers and is notoriously fragile. docker-compose alongside CI is manual, error-prone, and hard to clean up. Shared databases cause flaky tests. Skipping integration tests defeats the purpose.&lt;/p&gt;

&lt;p&gt;PikoCI takes a different approach: services as a first-class concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  What services are
&lt;/h2&gt;

&lt;p&gt;A service in PikoCI is an ephemeral process that runs alongside your job's tasks. Services start where they are defined in the job plan, if you define them first, they start first; if you define them after a &lt;code&gt;get&lt;/code&gt; step, they start after the &lt;code&gt;get&lt;/code&gt;. They always stop unconditionally when the job ends, regardless of where they were defined or whether tasks succeeded or failed.&lt;/p&gt;

&lt;p&gt;You define a &lt;code&gt;service_type&lt;/code&gt; once and reference it from any job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;service_type&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      NAME="pikoci-${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-pg"
      docker rm -f $NAME 2&amp;gt;/dev/null || true
      docker run -d --name $NAME -p 5432:5432 \
        -e POSTGRES_PASSWORD=test \
        postgres:$param_version
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;ready_check&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"docker exec pikoci-${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-pg pg_isready"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2s"&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"30s"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"docker rm -f pikoci-${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-pg 2&amp;gt;/dev/null || true"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"integration"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"16"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"make"&lt;/span&gt;
      &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"integration-test"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PikoCI starts Postgres, waits for &lt;code&gt;pg_isready&lt;/code&gt; to return 0, runs your tasks, then stops and removes the container. Your task connects to &lt;code&gt;localhost:5432&lt;/code&gt; like it would anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle
&lt;/h2&gt;

&lt;p&gt;Services start in the order they appear in the job plan. You control placement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define a &lt;code&gt;get&lt;/code&gt; step first if you need the code pulled before services start&lt;/li&gt;
&lt;li&gt;Define services wherever makes sense for your job&lt;/li&gt;
&lt;li&gt;After all steps complete, success or failure, &lt;code&gt;stop&lt;/code&gt; runs for every started service&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The stop step always runs. If your task panics, if the job is cancelled, if the worker gets a SIGTERM, stop still runs. No orphaned containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Docker-in-Docker
&lt;/h2&gt;

&lt;p&gt;Services don't run inside a container. They run on the worker host directly. Your tasks can run inside Docker containers via the docker runner, and they connect to services on &lt;code&gt;localhost&lt;/code&gt; because everything is on the same host network.&lt;/p&gt;

&lt;p&gt;This is the key difference from Docker-in-Docker. With DinD you're running a Docker daemon inside a container, which requires &lt;code&gt;--privileged&lt;/code&gt;, has kernel-level implications, and is generally fragile. With PikoCI services, Docker is just a process manager, the worker runs &lt;code&gt;docker run&lt;/code&gt;, the container starts on the host network, your tasks connect normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orphan prevention
&lt;/h2&gt;

&lt;p&gt;If a worker crashes mid-job, the &lt;code&gt;stop&lt;/code&gt; block never runs and Docker containers keep running. The next job tries to start on the same port and fails.&lt;/p&gt;

&lt;p&gt;The solution is stable container names with pre-start cleanup. Use &lt;code&gt;$BUILD_PIPELINE_NAME&lt;/code&gt; and &lt;code&gt;$BUILD_JOB_NAME&lt;/code&gt;, stable across runs, and always clean up at the start of the &lt;code&gt;start&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pikoci-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUILD_PIPELINE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUILD_JOB_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-postgres"&lt;/span&gt;
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;   &lt;span class="c"&gt;# kill orphan if exists&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$NAME&lt;/span&gt; ...           &lt;span class="c"&gt;# start fresh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| true&lt;/code&gt; means cleanup never fails the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not just Docker
&lt;/h2&gt;

&lt;p&gt;Services aren't Docker-specific. You can start any process: a local daemon, a background script, anything the worker can run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;service_type&lt;/span&gt; &lt;span class="s2"&gt;"redis"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"redis-server --daemonize yes --dir $WORKDIR"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"redis-cli shutdown"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ready check needed, Redis starts fast enough that the job can proceed immediately after start completes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sourceable and reusable
&lt;/h2&gt;

&lt;p&gt;Like all PikoCI abstractions, &lt;code&gt;service_type&lt;/code&gt; definitions are sourceable from a URL. Write a service type once, host it in a git repo or anywhere with an HTTPS endpoint, and reference it from any pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;service_type&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;#source = "https://raw.githubusercontent.com/myorg/pikoci-services/main/postgres.hcl"&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://postgresql"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The built-in types use the same mechanism, &lt;code&gt;pikoci://postgres&lt;/code&gt; is just a URL that ships with the binary. Community-built service types work identically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if my worker runs in Docker?
&lt;/h2&gt;

&lt;p&gt;If your worker runs as a Docker container, services that use &lt;code&gt;docker run&lt;/code&gt; need the Docker socket mounted so the worker spawns sibling containers on the host instead of nested ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock pikoci-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, if your worker's Docker image already has the service preinstalled (Postgres, MySQL, Redis) you can start it as a local process directly, no socket needed. It's a less common setup but works cleanly for fixed environments.&lt;/p&gt;

&lt;p&gt;The simplest approach is to run the worker as a plain process on the host. Then any service can use Docker freely, just the worker running containers, no nesting involved. That's how the PikoCI dogfooding pipeline runs its workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  PikoCI uses this itself
&lt;/h2&gt;

&lt;p&gt;The pipeline that tests PikoCI runs integration tests against six backends simultaneously using services: MariaDB, PostgreSQL, NATS, RabbitMQ, Kafka, and Vault. All six are defined in the job plan, all six stop when the job ends. The pipeline is publicly visible at ci.pikoci.com/teams/main/pipelines/pikoci, no account needed.&lt;/p&gt;




&lt;p&gt;This is how integration tests should work in CI. Not Docker-in-Docker, not shared test databases, not skipped tests. Just services that start where you need them and stop when the job ends.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pikoci.com" rel="noopener noreferrer"&gt;pikoci.com&lt;/a&gt; · &lt;a href="https://github.com/pikoci/pikoci" rel="noopener noreferrer"&gt;github.com/pikoci/pikoci&lt;/a&gt; · &lt;a href="https://docs.pikoci.com/Services/" rel="noopener noreferrer"&gt;docs.pikoci.com/Services&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>go</category>
      <category>docker</category>
    </item>
    <item>
      <title>How hard can it be to build a CI/CD system?</title>
      <dc:creator>Francesc Gil</dc:creator>
      <pubDate>Thu, 28 May 2026 22:31:06 +0000</pubDate>
      <link>https://dev.to/xescugc/how-hard-can-it-be-to-build-a-cicd-system-1cnj</link>
      <guid>https://dev.to/xescugc/how-hard-can-it-be-to-build-a-cicd-system-1cnj</guid>
      <description>&lt;p&gt;How hard can it be to build a CI/CD system?&lt;/p&gt;

&lt;p&gt;That question stuck with me long enough that I actually started building one. Not because someone asked me to. Not because I spotted a market gap. Just because the question wouldn't go away.&lt;/p&gt;

&lt;p&gt;The trigger was Concourse CI. I've been using it for a while and what I love about it is the resource abstraction, an interface that anything external has to follow. Check for new versions, pull them, push back. As a Go developer this kind of clean interface resonates with me. Everything in the pipeline is just something that implements that contract.&lt;/p&gt;

&lt;p&gt;But the operational overhead is significant. And I needed CI for my own side projects anyway, games and open source tools that require custom environments GitHub Actions can't provide. So I started building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted
&lt;/h2&gt;

&lt;p&gt;A single binary that could also scale horizontally when needed. Start with nothing, grow when you need to.&lt;/p&gt;

&lt;p&gt;You start like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./pikoci server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-system&lt;/span&gt; mem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pubsub-system&lt;/span&gt; mem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-worker&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pipeline-config&lt;/span&gt; pipeline.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a complete CI/CD system. In memory, no files, no external services. When you want persistence, add &lt;code&gt;--db-system sqlite&lt;/code&gt;. When you need distributed workers, add NATS and start workers on other machines. The pipeline config never changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Four pluggable abstractions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PikoCI has four concepts you define in HCL and can source from a URL: resource types, runners, service types, and secret types. Each follows the same pattern: define the type once, instantiate it with params.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;resource_type&lt;/strong&gt; defines how to watch something for changes and fetch it. A &lt;strong&gt;resource&lt;/strong&gt; is an instance of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://git"&lt;/span&gt;  &lt;span class="c1"&gt;# built-in&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="s2"&gt;"my-app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/org/app"&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;check_interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"@every 1m"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;runner_type&lt;/strong&gt; defines where tasks execute. The &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;exec&lt;/code&gt; runners are built-in, no declaration needed. Here's what the docker runner looks like under the hood, in case you want to define your own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# this is what pikoci://docker looks like&lt;/span&gt;
&lt;span class="c1"&gt;# define your own to customize or replace it&lt;/span&gt;
&lt;span class="nx"&gt;runner_type&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"$WORKDIR:/workdir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"-w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/workdir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"$image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"$cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="s2"&gt;"my-app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"run-tests"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"golang:1.25"&lt;/span&gt;
      &lt;span class="nx"&gt;cmd&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cd app &amp;amp;&amp;amp; make test"&lt;/span&gt;
      &lt;span class="nx"&gt;args&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/cache/go:/root/go/pkg/mod"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;secret_type&lt;/strong&gt; defines where credentials come from. Secrets are bound to variables and referenced anywhere in the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;secret_type&lt;/span&gt; &lt;span class="s2"&gt;"vault"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://vault"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="s2"&gt;"vault"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secret/data/db"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"password"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;service_type&lt;/strong&gt; defines processes that run alongside your tasks, started before, stopped after, guaranteed regardless of outcome. This is the feature I'm most proud of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;service_type&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# source = "pikoci://postgres" — or define inline&lt;/span&gt;
  &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"docker run -d --name db -p 5432:5432 postgres:16"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;ready_check&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pg_isready -h localhost"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"30s"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
    &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-ec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"docker rm -f db"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"integration"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nx"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="s2"&gt;"my-app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"make"&lt;/span&gt;
      &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"integration-test"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Docker-in-Docker. No docker-compose alongside CI. The service stops regardless of whether the job passed or failed.&lt;/p&gt;

&lt;p&gt;All four types are sourceable from a URL. The built-ins use &lt;code&gt;pikoci://&lt;/code&gt;, the same mechanism as anything you host yourself. If you need a runner that executes jobs in Kubernetes or Azure, write it once, host it anywhere, reference it by URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Here's a small pipeline that uses all four abstractions at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://git"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="s2"&gt;"my-app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/org/app"&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;check_interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"@every 1m"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;secret_type&lt;/span&gt; &lt;span class="s2"&gt;"vault"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://vault"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="s2"&gt;"vault"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secret/data/db"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"password"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;service_type&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pikoci://postgresql"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"git"&lt;/span&gt; &lt;span class="s2"&gt;"my-app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"17"&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5432"&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"run-tests"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"golang:1.25"&lt;/span&gt;
      &lt;span class="nx"&gt;cmd&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cd app &amp;amp;&amp;amp; make integration-test"&lt;/span&gt;
      &lt;span class="nx"&gt;args&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/cache/go:/root/go/pkg/mod"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A git resource watches for changes, a Vault secret feeds the Postgres password, Postgres starts as a service, and the task runs inside Docker. All four abstractions, one pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running pipelines locally&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pikoci run &lt;span class="nt"&gt;--pipeline-config&lt;/span&gt; pipeline.hcl &lt;span class="nt"&gt;--job&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any job, on your laptop, no server required. Override resources with local paths, inject secrets via &lt;code&gt;--var&lt;/code&gt;. The same pipeline that runs in CI runs on your laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue decision
&lt;/h2&gt;

&lt;p&gt;Workers don't connect directly to the server, they subscribe to a queue. This means workers can be behind NAT, on a laptop, in a different data center, or completely ephemeral. The server never needs to know where workers are. It made distributed workers trivially easy to add. Start a worker anywhere with network access to the queue and it just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1zg17gwturrjhnqf7gb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1zg17gwturrjhnqf7gb.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PikoCI deploys itself. The pipeline runs PR checks, mock tests, and integration tests against six different database and queue backends: MariaDB, PostgreSQL, NATS, RabbitMQ, Kafka, and Vault, all running as services. Then it builds multi-arch Docker images and redeploys itself with zero downtime.&lt;/p&gt;

&lt;p&gt;Those six backends are not just targets, they are the pluggable abstractions themselves. The same PikoCI binary connects to any of them depending on how you start it. Testing against all of them is how I make sure the abstractions actually hold.&lt;/p&gt;

&lt;p&gt;All of it is publicly visible at &lt;a href="https://ci.pikoci.com/teams/main/pipelines/pikoci" rel="noopener noreferrer"&gt;ci.pikoci.com/teams/main/pipelines/pikoci&lt;/a&gt;. No account needed.&lt;/p&gt;

&lt;p&gt;It's Apache 2.0, written in Go, and very much something I use for my own projects every day.&lt;/p&gt;

&lt;p&gt;If you try it and something is broken, I want to know. If something is missing, I also want to know.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pikoci.com" rel="noopener noreferrer"&gt;pikoci.com&lt;/a&gt; · &lt;a href="https://github.com/pikoci/pikoci" rel="noopener noreferrer"&gt;github.com/pikoci/pikoci&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>go</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
