<?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: dodger</title>
    <description>The latest articles on DEV Community by dodger (@dodger_one).</description>
    <link>https://dev.to/dodger_one</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%2F4115514%2F166398c4-a77a-4c02-af83-479b5c6bf37a.jpg</url>
      <title>DEV Community: dodger</title>
      <link>https://dev.to/dodger_one</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dodger_one"/>
    <language>en</language>
    <item>
      <title>Docker Wouldn't Give Me My Build Artifacts. So I Stole Them from OverlayFS.</title>
      <dc:creator>dodger</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:11:43 +0000</pubDate>
      <link>https://dev.to/dodger_one/docker-wouldnt-give-me-my-build-artifacts-so-i-stole-them-from-overlayfs-lki</link>
      <guid>https://dev.to/dodger_one/docker-wouldnt-give-me-my-build-artifacts-so-i-stole-them-from-overlayfs-lki</guid>
      <description>&lt;p&gt;&lt;em&gt;How I bypassed Docker's build abstraction to recover intermediate artifacts and rescue a CI/CD pipeline.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A few years ago, I was tasked with migrating an entire Atlassian Bamboo CI/CD infrastructure. And I don't mean just upgrading a few packages: the existing Bamboo version had been out of support for several years, the underlying hardware platform was being decommissioned, and to make things even more interesting, the Bamboo agents were running &lt;strong&gt;Red Hat Enterprise Linux 6&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Yes. RHEL 6.&lt;/p&gt;

&lt;p&gt;The objective was to migrate the infrastructure to a modern, supported environment while keeping the existing build pipelines operational.&lt;/p&gt;

&lt;p&gt;And one of those pipelines belonged to a bu$ine$$-critical internal application.&lt;/p&gt;

&lt;p&gt;The build process involved multiple stages, including Docker builds, C++ compilation, and static code analysis.&lt;/p&gt;

&lt;p&gt;Somewhere during the migration, we discovered that certain files generated inside Docker's intermediate build stages were needed by subsequent pipeline steps, but were not available in the final image.&lt;/p&gt;

&lt;p&gt;To make things more entertaining, the Dockerfile wasn't entirely under my control.&lt;/p&gt;

&lt;p&gt;I tried the conventional approaches. I even experimented with modifying parts of the build process from the pipeline itself.&lt;/p&gt;

&lt;p&gt;Nothing provided a satisfactory solution in that environment.&lt;/p&gt;

&lt;p&gt;And the requirement was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make it work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker multi-stage builds: now you see it, now you don't
&lt;/h2&gt;

&lt;p&gt;Multi-stage builds are wonderful for producing smaller, cleaner runtime images.&lt;/p&gt;

&lt;p&gt;You compile your application in one stage, copy what you need into another stage, and discard the build environment.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build-environment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /workspace&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;./build.sh

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; runtime-environment&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder \&lt;/span&gt;
    /workspace/bin/application \
    /app/application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect.&lt;/p&gt;

&lt;p&gt;Unless you need something else from the builder stage, and your existing build process doesn't provide an accessible way to retrieve it.&lt;/p&gt;

&lt;p&gt;In my case, part of the challenge involved C++ build outputs and information required by static analysis.&lt;/p&gt;

&lt;p&gt;One particularly interesting file was &lt;code&gt;compile_commands.json&lt;/code&gt;, a compilation database describing how source files were compiled.&lt;/p&gt;

&lt;p&gt;The problem wasn't that Docker couldn't support artifact extraction in general.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem was that I couldn't get the specific existing pipeline to give me what I needed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I even tried to modify the application's build process myself, despite it being completely outside my area of responsibility.&lt;/p&gt;

&lt;p&gt;When I brought the idea up with the application's development lead, he looked at me as if I had completely lost my mind.&lt;/p&gt;

&lt;p&gt;Apparently, suggesting changes to their build system was a crazier idea than digging through Docker's internal filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Well, challenge accepted.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing what's under the hood
&lt;/h2&gt;

&lt;p&gt;I've been working with Linux long enough to have developed a healthy distrust of abstractions.&lt;/p&gt;

&lt;p&gt;Containers aren't virtual machines.&lt;/p&gt;

&lt;p&gt;Underneath the packaging, namespaces, cgroups and runtime machinery, we're still dealing with Linux processes and filesystems.&lt;/p&gt;

&lt;p&gt;At the time, the Docker agents used the &lt;code&gt;overlay2&lt;/code&gt; storage driver.&lt;/p&gt;

&lt;p&gt;Which meant that Docker's internal filesystem data lived under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/docker/overlay2/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's where things got interesting.&lt;/p&gt;

&lt;p&gt;Just because a file wasn't available in the final Docker image didn't necessarily mean its data had already disappeared from the host.&lt;/p&gt;

&lt;p&gt;Docker's storage could still contain files from intermediate build layers.&lt;/p&gt;

&lt;p&gt;So I had a thought:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why ask Docker for the files when I could ask Linux?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first hack: finding artifacts directly
&lt;/h2&gt;

&lt;p&gt;I introduced a configurable variable into the CI/CD pipeline:&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;DOCKER_EXTRACT_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The pattern above is illustrative, not the actual proprietary filename or value used in the pipeline.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The actual extraction mechanism was wonderfully uncomplicated.&lt;/p&gt;

&lt;p&gt;A simplified, anonymized version of the original approach:&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;OVERLAY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/docker/overlay2"&lt;/span&gt;
&lt;span class="nv"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="o"&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;WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/out"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OVERLAY_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-mmin&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_EXTRACT_FILES&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="o"&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;OUTPUT_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;That's a &lt;code&gt;find&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Running against Docker's internal storage.&lt;/p&gt;

&lt;p&gt;From a production CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-mmin -5&lt;/code&gt; filter was intentional. I wanted recently modified files, not arbitrary historical leftovers from previous builds.&lt;/p&gt;

&lt;p&gt;The idea was to run the extraction immediately after &lt;code&gt;docker build&lt;/code&gt;, while the relevant filesystem data was still present.&lt;/p&gt;

&lt;p&gt;No Docker API.&lt;/p&gt;

&lt;p&gt;No additional container.&lt;/p&gt;

&lt;p&gt;No registry round trip.&lt;/p&gt;

&lt;p&gt;Just Linux filesystem access.&lt;/p&gt;

&lt;p&gt;And it worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second hack: recovering an entire build workspace
&lt;/h2&gt;

&lt;p&gt;The next challenge was more interesting.&lt;/p&gt;

&lt;p&gt;For C++ static analysis, I needed build information that had been generated inside Docker.&lt;/p&gt;

&lt;p&gt;The solution started by modifying CMake configuration before the Docker build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"CMakeLists.txt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'1 i set(CMAKE_EXPORT_COMPILE_COMMANDS ON)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enabled generation of &lt;code&gt;compile_commands.json&lt;/code&gt; in the relevant CMake builds.&lt;/p&gt;

&lt;p&gt;Once Docker finished building the image, I searched its storage for that file:&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;COMPILE_COMMANDS_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;
    &lt;span class="nb"&gt;sudo &lt;/span&gt;find /var/lib/docker/overlay2 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-mmin&lt;/span&gt; &lt;span class="nt"&gt;-4&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"compile_commands.json"&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me more than a filename.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It gave me a way to locate the build workspace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In that particular environment, I knew the workspace directory layout.&lt;/p&gt;

&lt;p&gt;By using the path of &lt;code&gt;compile_commands.json&lt;/code&gt;, I could identify the corresponding workspace location within Docker's storage and synchronize its contents back into Bamboo's working directory.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="nb"&gt;sudo &lt;/span&gt;rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="se"&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;RECOVERED_WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="se"&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;BAMBOO_WORKSPACE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Bamboo had access to build information that had previously been trapped inside Docker's intermediate filesystem data.&lt;/p&gt;

&lt;p&gt;I had effectively turned Docker's internal storage into an artifact extraction interface.&lt;/p&gt;

&lt;p&gt;An undocumented, implementation-dependent, absolutely unsupported interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But an interface nonetheless.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating the hack into the pipeline
&lt;/h2&gt;

&lt;p&gt;The beautiful part was that I didn't need a separate manual recovery procedure.&lt;/p&gt;

&lt;p&gt;The extraction became part of the ordinary build sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prepare source files
       |
       v
Patch CMake configuration
       |
       v
docker build
       |
       v
Recover C++ build workspace
       |
       v
Extract selected build files
       |
       v
Continue the normal CI/CD pipeline
       |
       v
Publish artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scripts ran on the Bamboo agent itself, with the permissions required to inspect the Docker storage.&lt;/p&gt;

&lt;p&gt;Everything was automated.&lt;/p&gt;

&lt;p&gt;No engineer manually copying files.&lt;/p&gt;

&lt;p&gt;No SSH session to rescue a broken build.&lt;/p&gt;

&lt;p&gt;The pipeline simply executed the recovery steps and continued.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it actually work?
&lt;/h2&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;Not just once.&lt;/p&gt;

&lt;p&gt;Not just during a proof of concept.&lt;/p&gt;

&lt;p&gt;I tested the approach repeatedly, integrated it into the pipeline, and deployed it into production.&lt;/p&gt;

&lt;p&gt;And it worked.&lt;/p&gt;

&lt;p&gt;Of course, there were caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct dependency on Docker's internal storage implementation.&lt;/li&gt;
&lt;li&gt;Reliance on the &lt;code&gt;overlay2&lt;/code&gt; storage driver.&lt;/li&gt;
&lt;li&gt;No guarantees about intermediate layer persistence.&lt;/li&gt;
&lt;li&gt;Potential race conditions during extraction.&lt;/li&gt;
&lt;li&gt;Possible collisions between concurrent builds.&lt;/li&gt;
&lt;li&gt;Risk of retrieving stale files from previous builds.&lt;/li&gt;
&lt;li&gt;Dependency on file modification timestamps.&lt;/li&gt;
&lt;li&gt;Elevated filesystem permissions required.&lt;/li&gt;
&lt;li&gt;Potential interference from Docker garbage collection and cleanup operations.&lt;/li&gt;
&lt;li&gt;No official Docker API or support for this approach.&lt;/li&gt;
&lt;li&gt;No portability across different Docker storage drivers or build backends.&lt;/li&gt;
&lt;li&gt;Potential breakage after Docker upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Did I care?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did it work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every. Single. Time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This wasn't a general-purpose artifact extraction framework.&lt;/p&gt;

&lt;p&gt;It was a deliberately narrow solution for a known build environment, tested extensively and deployed into production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The requirement wasn't to make Docker happy. The requirement was to make the pipeline work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;There's a reason I still believe that understanding Linux internals matters, even in an era of containers, orchestration, infrastructure as code and increasingly sophisticated developer platforms.&lt;/p&gt;

&lt;p&gt;Abstractions are useful.&lt;/p&gt;

&lt;p&gt;They're supposed to make our lives easier.&lt;/p&gt;

&lt;p&gt;But when an abstraction fails to provide what you need, understanding the layer underneath can make the difference between being stuck and finding a solution.&lt;/p&gt;

&lt;p&gt;Docker saw build layers.&lt;/p&gt;

&lt;p&gt;Bamboo saw a pipeline.&lt;/p&gt;

&lt;p&gt;I saw files on a Linux filesystem.&lt;/p&gt;

&lt;p&gt;And sometimes, that's all you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The requirement wasn't to produce an elegant solution. The requirement was to make the pipeline work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;There's a reason I believe that &lt;strong&gt;understanding Linux internals matters THE MOST&lt;/strong&gt;, even in an era of containers, orchestration, infrastructure as code, and increasingly sophisticated developer platforms.&lt;/p&gt;

&lt;p&gt;Abstractions are useful.&lt;/p&gt;

&lt;p&gt;They're supposed to make our lives easier.&lt;/p&gt;

&lt;p&gt;But they also hide what's really happening underneath.&lt;/p&gt;

&lt;p&gt;And that's where things get interesting.&lt;/p&gt;

&lt;p&gt;What some companies call &lt;strong&gt;"expected behavior"&lt;/strong&gt;, the rest of the world might call &lt;strong&gt;"unexpected behavior"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And what most people see as unexpected behavior, some of us see as &lt;strong&gt;a whole new world of possibilities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Just as shown in this article. ;-)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker saw build layers.&lt;/li&gt;
&lt;li&gt;Bamboo saw a pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I saw files on a Linux filesystem.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And sometimes, that's all you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The requirement wasn't to produce an elegant solution. The requirement was to make the pipeline work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I did.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>cicd</category>
      <category>linux</category>
    </item>
    <item>
      <title>Open-Source Oracle Database Performance History with PostgreSQL and Grafana</title>
      <dc:creator>dodger</dc:creator>
      <pubDate>Wed, 09 Sep 2026 10:47:06 +0000</pubDate>
      <link>https://dev.to/dodger_one/open-source-oracle-database-performance-history-with-postgresql-and-grafana-1am3</link>
      <guid>https://dev.to/dodger_one/open-source-oracle-database-performance-history-with-postgresql-and-grafana-1am3</guid>
      <description>&lt;p&gt;Oracle databases expose a huge amount of performance information through dynamic performance views. The difficult part is not obtaining a metric at this exact moment. It is retaining enough connected evidence to investigate what happened after the workload has already changed.&lt;/p&gt;

&lt;p&gt;That is the problem I built &lt;strong&gt;Harry Performance Scraper for Oracle Database&lt;/strong&gt; to solve.&lt;/p&gt;

&lt;p&gt;Harry is an open-source performance data pipeline. It periodically collects SQL statistics, session activity, waits, blocking information, execution plans and system metrics from Oracle Database, stores that history in PostgreSQL, and makes it available through Grafana or plain SQL.&lt;/p&gt;

&lt;p&gt;Grafana is the interface. The PostgreSQL repository is the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance monitoring needs more than current metrics
&lt;/h2&gt;

&lt;p&gt;A conventional monitoring stack is excellent at answering operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the database available?&lt;/li&gt;
&lt;li&gt;How much CPU is it using?&lt;/li&gt;
&lt;li&gt;Is a tablespace filling up?&lt;/li&gt;
&lt;li&gt;How many sessions are connected?&lt;/li&gt;
&lt;li&gt;Has a counter crossed an alerting threshold?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are important questions, and tools such as Prometheus are extremely good at answering them.&lt;/p&gt;

&lt;p&gt;Performance investigations require a different kind of information. If database activity increased twenty minutes ago, I want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which sessions were active;&lt;/li&gt;
&lt;li&gt;which SQL statements they were executing;&lt;/li&gt;
&lt;li&gt;which events they were waiting for;&lt;/li&gt;
&lt;li&gt;which modules and programs created the workload;&lt;/li&gt;
&lt;li&gt;whether sessions were blocking one another;&lt;/li&gt;
&lt;li&gt;which execution plans were involved;&lt;/li&gt;
&lt;li&gt;and how those facts changed during the same time window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A graph showing that active sessions increased is a metric. The identities of those sessions, their SQL, waits, plans and relationships are the evidence required to explain the graph.&lt;/p&gt;

&lt;p&gt;Once the sessions disconnect or begin doing something else, much of that evidence is gone unless something collected it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Harry separates the collection, storage and presentation layers:&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bvjph92q8zof4f0tvx8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bvjph92q8zof4f0tvx8.png" alt="Harry architecture" width="799" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The collectors run scheduled queries against Oracle dynamic performance views. Different types of information are collected at different intervals because not every fact changes at the same rate or has the same cost.&lt;/p&gt;

&lt;p&gt;The default schedule currently includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Default interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database activity history&lt;/td&gt;
&lt;td&gt;2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL statistics, sessions and blocking&lt;/td&gt;
&lt;td&gt;15 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational metrics&lt;/td&gt;
&lt;td&gt;1 minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL text and execution-plan discovery&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every interval is configurable per collector. A small development database and a busy production estate do not need to use the same sampling policy.&lt;/p&gt;

&lt;p&gt;Harry writes the collected information into a relational PostgreSQL schema. High-frequency samples are kept separate from SQL text, execution plans and other slowly changing data. Time-based data can be partitioned and retention is configurable.&lt;/p&gt;

&lt;p&gt;This matters because repeatedly copying the full SQL text or execution plan into every sample would waste storage and make the repository harder to query. Harry instead stores different kinds of evidence according to their own lifecycle and connects them through relational keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL instead of Prometheus?
&lt;/h2&gt;

&lt;p&gt;I use Prometheus extensively. It is an excellent choice for numeric time-series data, operational monitoring and alerting. Harry does not exist because Prometheus is bad at those jobs.&lt;/p&gt;

&lt;p&gt;It exists because an Oracle performance incident is not only a collection of numeric time series.&lt;/p&gt;

&lt;p&gt;The information involved includes high-cardinality identifiers and relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database and instance;&lt;/li&gt;
&lt;li&gt;session ID and serial number;&lt;/li&gt;
&lt;li&gt;SQL ID and plan hash value;&lt;/li&gt;
&lt;li&gt;module, action and program;&lt;/li&gt;
&lt;li&gt;wait class and wait event;&lt;/li&gt;
&lt;li&gt;blocking and blocked sessions;&lt;/li&gt;
&lt;li&gt;SQL text;&lt;/li&gt;
&lt;li&gt;execution-plan operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these values make reasonable dimensions for a metric. Treating all of them as labels, however, can create a large number of series. SQL text and execution plans are not natural metric labels at all.&lt;/p&gt;

&lt;p&gt;The questions asked during an investigation are relational as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show the SQL executed by the sessions contributing to this wait class.&lt;/li&gt;
&lt;li&gt;Compare the plans used by the same SQL during two different periods.&lt;/li&gt;
&lt;li&gt;Find the blockers behind the sessions visible in this activity spike.&lt;/li&gt;
&lt;li&gt;Group activity by module, program, event, SQL or session without deciding every future grouping when the data is ingested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL gives Harry a natural representation for that information. It also means the collected data is not locked inside Harry: DBAs can query it directly, join it with their own information, build new Grafana panels or export it using standard tools.&lt;/p&gt;

&lt;p&gt;Prometheus can continue monitoring the infrastructure and handling conventional metrics. Harry adds the detailed, relational history needed for Oracle performance analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harry's Database Activity History
&lt;/h2&gt;

&lt;p&gt;One of Harry's main collectors samples active database work every two seconds and builds &lt;strong&gt;Database Activity History (DAH)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DAH can be explored by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wait class;&lt;/li&gt;
&lt;li&gt;wait event;&lt;/li&gt;
&lt;li&gt;SQL ID;&lt;/li&gt;
&lt;li&gt;session;&lt;/li&gt;
&lt;li&gt;module;&lt;/li&gt;
&lt;li&gt;program;&lt;/li&gt;
&lt;li&gt;instance;&lt;/li&gt;
&lt;li&gt;and other collected dimensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corresponding Grafana dashboard presents the samples as an activity timeline with supporting SQL, session, wait, module and program breakdowns. Selecting a time range makes it possible to move from an overall workload spike to the SQL, sessions or waits that formed it.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffytzp4ap2e01e7u1w5dw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffytzp4ap2e01e7u1w5dw.png" alt="Database Activity History (DAH)" width="800" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not a summary generated after the incident. It is the sequence of samples retained while the activity was occurring.&lt;/p&gt;

&lt;p&gt;Harry can then correlate that window with SQL statistics and session history instead of stopping at “database activity was high.”&lt;/p&gt;

&lt;h2&gt;
  
  
  From activity to the SQL responsible
&lt;/h2&gt;

&lt;p&gt;The following demo workload shows how the &lt;strong&gt;Top Consumers&lt;/strong&gt; dashboard connects a selected SQL statement with its workload, I/O, plan history, full text and execution plan.&lt;/p&gt;

&lt;p&gt;From there, an investigation can continue into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;executions and elapsed time;&lt;/li&gt;
&lt;li&gt;CPU and wait contribution;&lt;/li&gt;
&lt;li&gt;buffer activity and reads;&lt;/li&gt;
&lt;li&gt;SQL text;&lt;/li&gt;
&lt;li&gt;plan hash values;&lt;/li&gt;
&lt;li&gt;execution-plan contents;&lt;/li&gt;
&lt;li&gt;plan changes across time.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr3vstrfjqb4epcqu8ih.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr3vstrfjqb4epcqu8ih.png" alt="Top Consumers dashboard" width="800" height="725"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Grafana is useful here because the time range and variables provide a fast way to navigate the repository. It is not mandatory. The same information remains accessible as relational data through PostgreSQL.&lt;/p&gt;

&lt;p&gt;That distinction is deliberate: dashboards should accelerate an investigation, not define the limits of what can be investigated.&lt;/p&gt;

&lt;h2&gt;
  
  
  ASH, AWR and Oracle licensing
&lt;/h2&gt;

&lt;p&gt;Harry's Database Activity History is not Oracle Active Session History.&lt;/p&gt;

&lt;p&gt;By default, Harry builds its own sampled history from non-ASH dynamic performance views. It does not present external sampling as a reimplementation of every semantic or capability provided by Oracle ASH or AWR.&lt;/p&gt;

&lt;p&gt;Optional ASH collection can be enabled in environments that are appropriately licensed and configured to use it. Because Oracle feature licensing depends on the environment and contract, operators must verify their own licensing position before enabling optional collectors that query licensed sources.&lt;/p&gt;

&lt;p&gt;Keeping this boundary explicit is important. The objective is to provide useful and transparent performance history, not to hide where the information originated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operating the collector
&lt;/h2&gt;

&lt;p&gt;Performance history only has value if the collector is available when the incident occurs. Harry therefore supports multiple collector instances with leader election and automatic failover.&lt;/p&gt;

&lt;p&gt;The PostgreSQL repository supports partitioned storage and configurable retention, allowing deployments to choose how much history to preserve. Grafana can also create native alerts directly from Harry's PostgreSQL data.&lt;/p&gt;

&lt;p&gt;The components remain independent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Oracle Database is the performance source.&lt;/li&gt;
&lt;li&gt;Harry performs scheduled collection.&lt;/li&gt;
&lt;li&gt;PostgreSQL retains and relates the evidence.&lt;/li&gt;
&lt;li&gt;Grafana provides dashboards and alerting.&lt;/li&gt;
&lt;li&gt;Standard SQL provides unrestricted direct access to the repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the deployment self-hosted and avoids a proprietary performance-data backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Harry
&lt;/h2&gt;

&lt;p&gt;Harry Performance Scraper for Oracle Database is open source and under active development.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project and documentation:&lt;/strong&gt; &lt;a href="https://harryperformance.com/" rel="noopener noreferrer"&gt;harryperformance.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Grafana demo:&lt;/strong&gt; &lt;a href="https://demo.harryperformance.com/" rel="noopener noreferrer"&gt;demo.harryperformance.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/OneClickDBA/harry-performance-scraper" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Published Grafana dashboard:&lt;/strong&gt; &lt;a href="https://grafana.com/grafana/dashboards/25761-harry-database-activity-history-for-oracle-database-dah/" rel="noopener noreferrer"&gt;Harry – Database Activity History for Oracle Database (DAH)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am particularly interested in feedback from DBAs operating real Oracle workloads:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which piece of evidence do you most often discover was missing only after a performance incident had already ended?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>oracle</category>
      <category>observability</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
