← Back to Blog
pullmdclaude-codepwadockerMarch 29, 2026 · 7 min read

PullMD Part 2: From Spec to Running Container

SPEC → TESTS FIRST → CONTAINER · RUNNINGAI-generated image
Editorial diagram: a glowing SPEC portal on the left, a 'TESTS FIRST' banner above four stacked test cards (NORMALIZE, FETCH, FALLBACK, INTEGRATION), then a CONTAINER panel on the right with a green RUNNING indicator; a small status panel bottom-left tracks TDD · REVIEW · GREEN.Editorial diagram: a glowing SPEC portal on the left, a 'TESTS FIRST' banner above four stacked test cards (NORMALIZE, FETCH, FALLBACK, INTEGRATION), then a CONTAINER panel on the right with a green RUNNING indicator; a small status panel bottom-left tracks TDD · REVIEW · GREEN.

We had put together the specification from Part 1 on the evening of March 2. Architecture thought through, API design settled, comment format decided.

I handed it to Claude Code and reckoned it would first set up an Express server, implement the Reddit logic and maybe add a few tests somewhere later on.

But the first thing Claude created was tests. No server, no Reddit logic, none of that.

I did know that test-driven development (TDD) is firmly baked into the Superpowers skills. Actually seeing Claude start with the tests that consistently was still interesting.

Four Tests

They covered exactly what would later make up the architecture of the tool:

  • URL normalization as its own module
  • Reddit JSON fetch via mocks
  • HTML fallback in case the JSON route gets blocked
  • a separate integration test for real Reddit calls

So the tests didn’t just describe “a tool that fetches Reddit”.

They described a tool that first normalizes URLs reliably, fetches Reddit across several fallback stages and keeps fast unit tests clearly separated from slow integration tests.

The architecture was essentially settled before the actual implementation began.

How Claude Code Went About It

While writing this article I went into the session logs in JSONL format to see what Claude Code actually did that evening.

A pipeline of several Superpowers skills was running:

  1. The specification from the concept chat on claude.ai was broken down by a planning skill into numbered tasks: “Task 1: npm dependencies”, “Task 2: URL Normalization Module”, “Task 3: Redirect Resolution” and so on up to roughly Task 6.
  2. For each task a new subagent was started via subagent-driven-development. It got only its own concrete task description – not the full session and not the contents of the other tasks.
  3. Every implementer subagent was explicitly instructed to work by TDD: first write a failing test, check that it really fails, build the minimal implementation, run the test again and then commit.
  4. After each task, another subagent checked whether the implementation actually matched the specification.

The briefs from that night are in the logs, fairly precisely:

You are implementing Task 2: URL Normalization Module … with TDD

Then came “Task 3: Redirect Resolution”, “Task 5: Markdown Formatting” and “Spec compliance review for Task 6”.

So this wasn’t a single flash of insight from Claude Code, but the mechanical result of several TDD implementations with a review stage after each one.

The Skill Stack

“Tests first” wasn’t default behavior that happened to emerge here. The workflow explicitly enforced it.

Anyone who wants to set up a new project with Claude Code in a similar way essentially needs four skills from the Superpowers plugin:

  • brainstorming clarifies before the implementation what is to be built at all, which constraints apply and which decisions have to be made. In my case the concept chat on claude.ai had already taken that job.
  • writing-plans breaks the specification down into numbered, manageable tasks that can be implemented and tested one by one.
  • subagent-driven-development starts a fresh subagent for every task, one that only gets the context of its own task.
  • test-driven-development forms the inner loop: failing test, minimal implementation, green test, commit.

Of course you can also just tell Claude Code: “Write the tests first.”

Formally that’s the same thing. But without a plan broken down beforehand, the clear boundaries that meaningful tests can attach to are often missing. The skill stack supplies not just the instruction to write tests, but also the structure that gives them a concrete meaning.

Dockerfile, Compose and .dockerignore

That same night the service moved into a container.

The Dockerfile uses an ordinary Node.js image. No headless browser, no second process and no needlessly complicated construction.

At that point the service had to do exactly three things:

  1. fetch Reddit JSON
  2. convert the data to markdown
  3. output the result

A single lean Node.js container was entirely enough for that.

The docker-compose.yml got the usual Traefik labels: a subdomain from my network’s internal scheme and the automatic certificate resolver.

The service runs on my Ubuntu Docker host behind Traefik. Traefik handles TLS and routes the subdomain to the container.

That is the same setup I use for my other self-hosted services: start the container, Traefik reads the labels, issues the certificate and the service is reachable.

The .dockerignore stayed correspondingly small:

node_modules/
data/
.git/

Nothing more was needed. The service had no build artifacts and no local state that belongs in the image.

The result was a clean image that could be started on the host with a plain docker compose up -d.

The PWA with Web Share Target

The PWA was part of that same night. And that is exactly the part I had originally built the tool for.

A Progressive Web App, PWA for short, is basically an ordinary web application that can be installed on a phone like an app. It gets its own icon on the home screen, opens without the usual browser chrome and can use certain functions of the operating system.

That takes no native Android app, no Play Store and no separate codebase. The existing web interface is extended with the necessary app information through a manifest and has to be reachable over HTTPS.

For PullMD the Web Share Target API was the interesting part. It lets an installed PWA register itself as a target in the Android share menu.

The decisive part of the manifest looked like this:

"share_target": {
  "action": "/share",
  "method": "GET",
  "params": {
    "title": "title",
    "text": "text",
    "url": "link"
  }
}

If you share a link with the PWA, Android passes the title, the text and the URL to the /share endpoint. PullMD takes the Reddit link out of that, starts the extraction automatically and then displays the finished markdown.

In practice that meant:

Install the PWA, open the Reddit app, pull up a post and tap “Share”.

PullMD then appears in the Android share menu next to WhatsApp, Signal and the other usual suspects.

A tap on the entry opens the PWA, the extraction runs and shortly afterwards the markdown is on the screen.

Copy button. Done.

The First Aha Moment on the Phone

I still remember pretty exactly when it worked completely for the first time.

I had a longer thread open in the Reddit app, on an interesting topic I wanted to talk to Claude about. So I tapped “Share”, picked the service – and then the clean markdown was simply there.

No navigation, no cookie banner, no sidebar. Just the post and the comments, indented by their respective depth.

My first thought wasn’t even: “Cool, it works.”

It was more like: wow, that is a lot faster than I expected.

The old routine was:

Copy the URL, switch to another tab, paste the address and wait for the result.

Now it was one tap in the share menu, and the markdown was there.

At first that only sounds like a few saved steps. In practice it is exactly the difference between a tool you actually use and one where you think every time: ah, for that I would have to go and …

The Tests Paid Off Later

Over the following weeks the test files kept growing along with the project.

Every time we changed something larger in the URL handling, the comment format or the fallback logic, the corresponding tests failed first.

That showed Claude right away which previously working cases a change would have broken along the way – before it landed on the server.

The test suite that had come out of that first night almost incidentally, through the skill stack, proved itself in everyday use as well.

Precisely because the tests were not stuck onto the finished application afterwards, they mapped the original architecture decisions fairly closely.

What Came Next

After that the service just ran along quietly. Mostly on the phone, and for now only for me.

What was written down nowhere the whole time: what you can actually call on the thing.

→ PullMD Part 3: The API, and How Reddit Became Any URL


Part 2 of 15 in the PullMD series.

← PullMD Part 1: Ctrl+A and the Junk Context | PullMD Part 3: The API, and How Reddit Became Any URL →

Comments

Commenting takes an email confirmation or a GitHub login. Comments run on my own server, not on an ad provider's — details in the privacy policy.