DEV Community

Cover image for I Got Tired of Googling Docker Errors. So I Built a Tool That Fixes That.
Michael Mbita
Michael Mbita

Posted on

I Got Tired of Googling Docker Errors. So I Built a Tool That Fixes That.

Every Docker error I've ever hit has followed the same ritual.

*The daemon spits out something like this:
*

  • docker: Error response from daemon: Bind for 0.0.0.0:8080 failed:
  • port is already allocated.
  • See 'docker run --help '

I stare at it. I copy it. I open a new tab. I paste it into Google.
I scan five Stack Overflow threads. I find the fix buried in a comment
from 2019. I run it. I move on.

I did this so many times I started to feel like I was failing some kind
of basic competence test. Surely there's a better way. Surely Docker
could just... tell me what's wrong.

It doesn't. So I built something that does.

**Introducing bugtalk

**
**bugtalk **is a transparent terminal wrapper that sits in front of your
docker binary and translates failures into plain-English fixes in
real time.

This is what hitting a port conflict looks like after you install it:

$ docker run -p 8080:80 nginx

docker: Error response from daemon: Bind for 0.0.0.0:8080 failed:
port is already allocated.

πŸ”§ [PORT_CONFLICT] Port 8080 is already in use by another process
πŸ’‘ Fix: sudo lsof -ti:8080 | xargs kill -9
⚠️  Risk: medium β€” review before running
Enter fullscreen mode Exit fullscreen mode

The raw Docker error still shows β€” you might want it for logs. bugtalk
adds the fix underneath. Right there. In your terminal. No tab
switching. No Googling. No Stack Overflow from 2019.

To install it:

pip install bugtalk
bugtalk setup
Enter fullscreen mode Exit fullscreen mode

Restart your shell. That's it. Docker commands work exactly as
before β€” same exit codes, same TTY sessions, same CI/CD pipelines.
You just never google a Docker error again.


How it works under the hood

The core idea is simple: prepend a wrapper script to your PATH that
intercepts Docker commands, captures stderr on failure, and matches
it against a library of known patterns.

Here's the full flow:

User types: docker run -p 8080:80 nginx
                    ↓
PATH resolution: /usr/local/lib/bugtalk/bin/docker  ← wrapper
                    ↓
Wrapper finds real docker via stored absolute path
(e.g. /usr/local/bin/docker or /opt/homebrew/bin/docker)
                    ↓
Runs real docker with original args, captures stderr
                    ↓
Exit code != 0 β†’ match stderr against errors.json
                    ↓
Pattern found β†’ print plain-English fix to stderr
                    ↓
Exit with original exit code (CI/CD unaffected)
Enter fullscreen mode Exit fullscreen mode

The real docker binary path is stored as an absolute path during
bugtalk setup β€” never a bare "docker" string. That one decision
prevents infinite recursion, which is the most common way this class
of tool fails.

The TTY problem

The trickiest part of building this was interactive commands.
docker exec -it container bash needs a live terminal. If you capture
stdin/stdout naively, the session hangs or errors.

bugtalk solves this by detecting TTY flags before deciding whether
to capture:

def _is_interactive_command(cmd, rest):
    # Check for -i, -t, -it, --interactive, --tty
    if _has_tty_flags(rest):
        return True
    # Also check if stdin is actually a terminal
    if cmd in ("exec", "run") and sys.stdin.isatty():
        return True
    return False
Enter fullscreen mode Exit fullscreen mode

If the command is interactive, bugtalk calls os.execv() β€” complete
process replacement, no capture, zero interference. Your shell session
works exactly as if bugtalk wasn't there.

Keeping CI/CD safe

This was non-negotiable. If bugtalk ever returned exit code 0 for a
failing docker command, it would silently swallow failures in
production pipelines. Unacceptable.

The wrapper always exits with docker's original exit code:

result = subprocess.run([real_docker] + args, capture_output=True, text=True)
# ... translate the error ...
sys.exit(result.returncode)  # always the original code
Enter fullscreen mode Exit fullscreen mode

A CI pipeline running docker run myimage pytest will still fail
exactly as expected. bugtalk just adds context on stderr.

Pattern matching with named capture groups

Each error pattern in errors.json uses named regex groups so the
fix command can reference extracted values directly:

"PORT_CONFLICT": {
  "regex": "Bind for [^:]+:(?P<port>\\d+) failed|port is already allocated",
  "message": "Port {port} is already in use by another process",
  "fixes": {
    "darwin": "lsof -ti:{port} | xargs kill -9",
    "linux":  "sudo lsof -ti:{port} | xargs kill -9",
    "windows": "netstat -ano | findstr :{port}",
    "default": "lsof -ti:{port} | xargs kill -9"
  },
  "risk_level": "medium"
}
Enter fullscreen mode Exit fullscreen mode

The translate() function builds a defaultdict(str) from both
positional and named capture groups, then uses format_map β€” so a
template with {port} never crashes if the port wasn't captured in
that particular alternation branch.

Auto-updates that don't block your terminal

New error patterns ship via a errors.json update on GitHub. The
wrapper checks for updates weekly β€” but crucially, this never adds
latency to your commands:

def schedule_update():
    t = threading.Thread(target=_do_update, daemon=True)
    t.start()  # returns immediately

def main():
    schedule_update()  # fires and forgets
    # rest of wrapper runs at full speed
Enter fullscreen mode Exit fullscreen mode

The update runs in a daemon thread in the background. If the network
is down, it fails silently and tries again next week. Your terminal
is never waiting on it.


What v1 covers

25 error patterns covering the errors I hit most often:

Pattern What it catches
PORT_CONFLICT Port already allocated
DAEMON_NOT_RUNNING Docker daemon not started
IMAGE_NOT_FOUND Image not pulled locally
VOLUME_PERMISSION Permission denied on mounted volume
PULL_RATE_LIMIT Docker Hub rate limit hit
DISK_SPACE No space left on device
CGROUP_OOM Container OOM-killed
MANIFEST_PLATFORM No image for this CPU architecture
NETWORK_CONFLICT Network already exists
EXEC_NOT_RUNNING exec on a stopped container
HEALTHCHECK_FAIL Container healthcheck failing
SECCOMP_DENIED Syscall blocked by seccomp
BUILD_NO_DOCKERFILE No Dockerfile in build context
LAYER_CACHE_MOUNT BuildKit not enabled
DNS_RESOLUTION DNS failure inside container
AUTH_REQUIRED Registry login needed
...and 9 more

OS-specific fixes for each β€” the command to kill a process on macOS
is different from Linux, and bugtalk knows which one to print.


The design decisions I'm most proud of

It never modifies the real docker binary. The original docker
is untouched. bugtalk only adds a wrapper earlier in PATH. Running
bugtalk unsetup removes the wrapper and cleans the PATH entry β€” a
complete, clean uninstall. Nothing left behind.

It fails safe. Every code path in the wrapper is wrapped in a
try/except that falls back to calling real docker directly via
os.execv. If bugtalk crashes, it gets out of the way and lets
Docker run normally. The worst case is you see a raw error message
instead of a translated one.

It has no runtime dependencies. The wrapper uses only Python's
standard library β€” subprocess, re, threading, json,
urllib.request. No third-party packages. Nothing to break. Nothing
to update separately from bugtalk itself.

Unknown errors ask for help. When stderr doesn't match any
known pattern, bugtalk nudges you to run bugtalk report, which
opens a pre-filled GitHub issue in your browser. No token required,
no copy-pasting. That's how the pattern library grows over time.


What I learned building it

The PATH wrapper pattern is underused. Sitting transparently in
front of a binary and intercepting failures is a surprisingly powerful
primitive. The same approach could work for npm, git, kubectl, pip β€”
any CLI tool with cryptic error messages. bugtalk v1 is Docker-only,
but the architecture is deliberately generic.

Interactive terminal detection is subtle. My first version broke
docker exec -it silently β€” the session would hang because
capture_output=True destroyed the TTY. The fix required checking
both the command flags and whether stdin was actually a terminal.
Seemingly obvious in hindsight. Very non-obvious at 2am.

The moat isn't the regex patterns. Anyone can copy a
errors.json. The moat is distribution β€” having a deployed piece of
software on developers' machines that intercepts their Docker commands.
That's a platform. The patterns are just the first thing that platform
does.


Install it

pip install bugtalk
bugtalk setup
Enter fullscreen mode Exit fullscreen mode

Restart your shell. Then trigger a Docker error you've seen before and
watch it get translated.

If it catches something that saves you a Google search, run:

bugtalk status   # see what version and how many patterns you have
Enter fullscreen mode Exit fullscreen mode

If it misses an error, run:

bugtalk report   # opens a pre-filled GitHub issue
Enter fullscreen mode Exit fullscreen mode

That's how v2's pattern list gets built.


Links


If you've ever copy-pasted a Docker error into Google, this is for
you. Would love to hear which errors you hit most β€” drop them in the
comments and I'll make sure they're in the next update.

Top comments (0)