DEV Community

Cover image for My Dev Environment Hated Me (And How I Fixed It)
Jose Rodriguez Marrero
Jose Rodriguez Marrero

Posted on

My Dev Environment Hated Me (And How I Fixed It)

A returning developer's battle with devcontainers, Docker image tags, and OAuth callbacks that ghost you like a bad Tinder date.


I'm back. After a few months away to deal with a family medical situation (more on that at the end), I've returned to the industry with a mission: build portfolio-worthy, production-grade projects and sharpen my .NET skills back to a competitive level.

My first project? A Feature Flag Service — a clean architecture .NET API that evaluates feature flags deterministically using rollout strategies like percentage-based rollouts and role-based targeting. Think LaunchDarkly, but built by me, from scratch, with a focus on solid engineering principles.

Before writing a single line of business logic, I had to do something deceptively simple: set up my dev environment.

Two hours later, I had four separate errors and a story worth writing about.


The Setup

The plan was clean and modern:

  • Devcontainer for a reproducible, containerized development environment
  • Claude Code inside the container for AI-assisted development
  • .NET 10 because why not use the latest?
  • VSCode Dev Containers extension to tie it all together

Here's what my devcontainer.json looked like going in:

{
  "name": "Feature Flag Service",
  "image": "mcr.microsoft.com/devcontainers/dotnet:1-10.0-bookworm",
  "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
    "ghcr.io/devcontainers/features/node:1": { "version": "lts" }
  },
  "forwardPorts": [5000, 5001],
  "postCreateCommand": "dotnet restore && dotnet tool restore"
}
Enter fullscreen mode Exit fullscreen mode

Looks reasonable, right? Narrator: It was not reasonable.


Bug #1 — The Image That Doesn't Exist

Error response from daemon: failed to resolve reference 
"mcr.microsoft.com/devcontainers/dotnet:1-10.0-bookworm": not found
Enter fullscreen mode Exit fullscreen mode

Right out of the gate. Docker couldn't pull the image because the tag 1-10.0-bookworm simply does not exist.

Here's the thing about Docker image tags — they're not automatically generated for every version of every framework. The mcr.microsoft.com/devcontainers/dotnet image only has published tags up to 8.0 and 9.0. No 10.0 yet.

But there's a second twist: .NET 10 dropped Debian (Bookworm) entirely. Starting with .NET 10, Microsoft switched the default base OS to Ubuntu. So even if the tag existed, -bookworm would be wrong.

The fix: Drop down to the latest available devcontainer image:

"image": "mcr.microsoft.com/devcontainers/dotnet:9.0"
Enter fullscreen mode Exit fullscreen mode

Lesson: Always verify Docker image tags exist before assuming they do. The pattern <language>:<version>-<os> isn't guaranteed — check the registry.


Bug #2 — The OAuth Callback That Ghosted Me (Main Event)

This one is the real star of today's post. Container built successfully. Claude Code installed. Time to log in.

claude auth login --claudeai
Enter fullscreen mode Exit fullscreen mode

Browser opens. I authenticate. I'm redirected back to VSCode and... nothing. The terminal just hangs. Waiting. Forever.

Eventually:

Failed to retrieve auth status after login
Enter fullscreen mode Exit fullscreen mode

What's actually happening here is a fascinating networking problem.

The Root Cause

When you run claude auth login, Claude Code spins up a localhost callback server on a random ephemeral port. The OAuth flow works like this:

  1. Claude Code starts listening on localhost:RANDOM_PORT
  2. Your browser opens claude.ai/oauth/authorize
  3. You authenticate in the browser
  4. The browser is redirected back to localhost:RANDOM_PORT/callback
  5. Claude Code receives the token and stores it

The problem? You're inside a container. The browser lives on your host machine. The callback server lives inside the container. That random port is never forwarded, so the callback from the browser never arrives. Claude Code sits there waiting for a knock on a door that nobody can reach.

HOST                          DEVCONTAINER
┌──────────────┐              ┌─────────────────────┐
│   Browser    │──callback──X─▶│  claude auth login  │
│              │  :RANDOM      │  (listening on      │
└──────────────┘  NOT FORWARDED│   RANDOM port)      │
                               └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The Fix — Bind Mount Your Host Credentials

The cleanest solution: log in on your host machine once, then mount your credentials into the container.

Claude Code stores OAuth tokens in two places on your filesystem:

  • ~/.claude/ — credentials, config, session data
  • ~/.claude.json — onboarding state (easy to miss this one!)

Update your devcontainer.json mounts:

"mounts": [
  "source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind",
  "source=${localEnv:HOME}/.claude.json,target=/home/vscode/.claude.json,type=bind"
]
Enter fullscreen mode Exit fullscreen mode

Then on your host machine (not inside the container):

claude auth login
Enter fullscreen mode Exit fullscreen mode

Complete the OAuth flow normally. Rebuild the container. Done — Claude Code inside the container picks up your credentials automatically, no login required.

Lesson: OAuth callback flows assume the browser and the listening server are on the same machine. In containerized environments, that assumption breaks. Bind mounts are your friend.


Bug #3 — postCreateCommand Needs a Map

postCreateCommand failed with exit code 1
Enter fullscreen mode Exit fullscreen mode

Vague. Unhelpful. Classic.

Running the commands manually revealed the culprit:

dotnet restore
# error: Specify a project or solution file.
Enter fullscreen mode Exit fullscreen mode

The container doesn't automatically know which solution to restore. A simple fix:

"postCreateCommand": "dotnet restore FeatureFlagService.sln && dotnet tool restore"
Enter fullscreen mode Exit fullscreen mode

Lesson: Be explicit in automation. Commands that work fine interactively (where you're already in context) can fail in scripts because that context doesn't exist.


Bug #4 — The SDK vs. Target Framework Mismatch

error NETSDK1045: The current .NET SDK does not support targeting .NET 10.0.
Either target .NET 9.0 or lower, or use a version of the .NET SDK that supports .NET 10.0.
Enter fullscreen mode Exit fullscreen mode

Remember how we switched to the .NET 9 devcontainer image? Right. The SDK is .NET 9. The project targets .NET 10. They don't agree.

Rather than installing a custom SDK (more complexity, more maintenance), I downgraded all project files to target net9.0:

find . -name "*.csproj" | xargs sed -i \
  's/<TargetFramework>net10.0<\/TargetFramework>/<TargetFramework>net9.0<\/TargetFramework>/g'
Enter fullscreen mode Exit fullscreen mode

One command. All five .csproj files updated. Verified with:

grep -r "TargetFramework" --include="*.csproj"
Enter fullscreen mode Exit fullscreen mode

Lesson: Your container image's SDK version and your project's target framework must align. When they don't, decide which one you're willing to move — usually it's easier to adjust the project than rebuild the image.


The Final devcontainer.json

Here's where we landed after all four fixes:

{
  "name": "Feature Flag Service",
  "image": "mcr.microsoft.com/devcontainers/dotnet:9.0",

  "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
    "ghcr.io/devcontainers/features/node:1": { "version": "lts" }
  },

  "forwardPorts": [5000, 5001],
  "portsAttributes": {
    "5000": { "label": "HTTP", "onAutoForward": "notify" },
    "5001": { "label": "HTTPS", "onAutoForward": "notify" }
  },

  "mounts": [
    "source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind",
    "source=${localEnv:HOME}/.claude.json,target=/home/vscode/.claude.json,type=bind"
  ],

  "containerEnv": {
    "CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
    "ASPNETCORE_ENVIRONMENT": "Development"
  },

  "remoteUser": "vscode",
  "containerUser": "vscode",

  "postCreateCommand": "dotnet restore FeatureFlagService.sln && dotnet tool restore",
  "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",

  "customizations": {
    "vscode": {
      "extensions": [
        "ms-dotnettools.csharp",
        "ms-dotnettools.csdevkit",
        "humao.rest-client",
        "eamodio.gitlens",
        "editorconfig.editorconfig",
        "streetsidesoftware.code-spell-checker"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.tabSize": 4,
        "files.eol": "\n",
        "dotnet.defaultSolution": "FeatureFlagService.sln"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

Four bugs. One working dev environment. Two hours I'll never get back — but also two hours that gave me a much better understanding of how Docker networking, OAuth flows, and devcontainer configuration actually work under the hood.

The Feature Flag Service itself is still ahead of me. Clean Architecture layers to wire up, a deterministic hashing strategy to implement, unit tests to write. But the foundation is solid, the tooling is in place, and Claude Code is authenticated and ready to pair program.

Next post: getting into the actual code — implementing the evaluation engine and the strategy pattern at the heart of this thing.


A note on the comeback: I stepped away from software development for a couple of years to deal with a family medical situation. That chapter is now closed, and I'm back — sharper, more intentional, and honestly more motivated than ever. If you're a developer who's taken time away for life reasons, this blog is partly for you. It's possible to come back. Let's build.


Tags: #dotnet #devcontainers #docker #claudecode #webdev #programming #cleanarchitecture #debugging

Top comments (0)