[{"content":"This week I went to the Devin Mixin / Hackathon event hosted at the Cloudflare offices in London. We were gifted a chunk of credits to use with Devin during the hackathon, and I decided to fork and add some features I wanted to krisfoster/ccusage. ccusage is an open-source CLI for tracking token consumption and cost across coding assistants and I find it super useful.\nI wanted to add two features that I felt were missing. First, aggregating token logs across separate development machines (into Google Cloud Storage - GCS), and secondly generating a nice HTML dashboard to show my aggregate profligate spend. Oh, and I want the dashboard to show me how much I could have saved by switching LLM provider.\nNote: I have a Claude Pro sub, I don\u0026rsquo;t really spend that much on AI! ccusage uses my tokens and gives me a token-based cost.\nThe Goal: Cross-Machine Aggregation and Visual Reports ccusage reads local session logs from agent data directories on your filesystem, meaning that when you work across multiple laptops, or remote development boxes, usage metrics remain isolated on each machine. And it is CLI first for reports. So my plan, in detail, was to add:\nCloud storage sync: Syncing raw JSON logs from each workstation to a centralized Google Cloud Storage bucket. Dashboard generation and sharing: Generating a standalone HTML dashboard directly from synced cloud data, complete with shareable signed URLs. Building Asynchronously with Devin I pointed Devin at the forked repository and started planning how to build. And although my project took longer than I had in the hackathon, I ended up being pretty happy with the result.\nThe most noticeable difference when working with a cloud-hosted agent is detachment from the local process. Local coding assistants require an active terminal session, keeping the IDE open and supervising command execution directly. Because Devin runs entirely in its own remote sandbox, I submitted prompt batches, closed the laptop lid, and let the agent work while chatting with other attendees at the event.\nSo, what did Devin and I build?\n1. Bucket Synchronization A backend sync command to upload and reconcile local usage records with a configured Google Cloud Storage bucket:\nccusage sync setup # Sets up cloud storage, you do need a Google Cloud account. ccusage sync run # Run the sync 2. Shareable Signed URLs To share dashboards securely without opening public bucket permissions, Devin added automated URL signing:\nccusage sync share The generated links provide time-bounded read access directly to the static dashboard asset in GCS.\n3. Dashboard Deployment To view aggregate statistics across machines and models, Devin generated a static single-page report containing spend summaries, token totals, and daily cost breakdowns:\nccusage sync dashboard --deploy --open Get the Code You can find the code on GitHub: krisfoster/ccusage.\n","permalink":"/hacking-ccusage-with-devin/","summary":"\u003cp\u003eThis week I went to the Devin Mixin / Hackathon event hosted at the Cloudflare offices in London. We were gifted a chunk of credits to use with Devin during the hackathon, and I decided to fork and add some features I wanted to \u003ca href=\"https://github.com/krisfoster/ccusage\"\u003ekrisfoster/ccusage\u003c/a\u003e. ccusage is an open-source CLI for tracking token consumption and cost across coding assistants and I find it super useful.\u003c/p\u003e\n\u003cp\u003eI wanted to add two features that I felt were missing. First, aggregating token logs across separate development machines (into Google Cloud Storage - GCS), and secondly generating a nice HTML dashboard to show my aggregate profligate spend. Oh, and I want the dashboard to show me how much I could have saved by switching LLM provider.\u003c/p\u003e","title":"Hacking on ccusage with Devin: Cloud Sync and HTML Dashboards"},{"content":"","permalink":"/chinatown/","summary":"","title":""},{"content":"","permalink":"/canal-sharks/","summary":"","title":""},{"content":"Most container tutorials isolate individual tools. You read one guide on writing multi-stage Dockerfiles, another on setting up Compose networking, and a third on integration testing. Seeing how these pieces integrate into a cohesive, production-grade architecture is much rarer.\nThe open-source repository krisfoster/crossy-whale demonstrates this more complete workflow through a playable demo. It is a browser-based arcade game starring the Docker whale navigating lanes of container traffic, built in Go and vanilla JavaScript. Beneath the game sits an architecture showcasing six core container technologies in a single repository.\n1. Multi-Service Orchestration with Docker Compose Running the entire stack locally requires one command:\ndocker compose up -d Open docker-compose.yml. Compose coordinates five core services on a shared private bridge network:\nservices: redis: # In-memory data store for leaderboards and QR state app: # Core Go web backend and game server commits-service: # Microservice streaming recent git commits via SSE scores-service: # Microservice managing score submissions nginx: # Reverse proxy and static file ingress ngrok: # Optional public HTTPS tunnel Internal DNS and Network Isolation Compose automatically generates internal DNS entries matching each service name. The Go backend connects to Redis using the hostname redis:\n# app service definition environment: - REDIS_ADDR=redis:6379 No hardcoded container IP addresses exist in the codebase. Docker resolves redis dynamically on the stack network.\nNetwork boundaries remain strict:\nredis: expose: - \u0026#34;6379\u0026#34; nginx: ports: - \u0026#34;80:80\u0026#34; app: ports: - \u0026#34;8080:8080\u0026#34; The expose directive allows inter-container communication on port 6379 while preventing the port from binding to your host interface. Nginx acts as the single public entry point on port 80, routing API calls and serving static assets.\nConditional Profiles The stack supports optional services via Compose profiles. To expose the game to mobile devices over the internet, pass --profile public:\ndocker compose --profile public up -d The ngrok service declaration uses profiles: [public], preventing unnecessary tunnels from starting during standard local development.\n2. Lean Multi-Stage Dockerfiles Open app/Dockerfile. The Go backend uses a two-stage build pattern to produce a minimal runtime image:\n# Stage 1: Compilation environment FROM dhi.io/golang:1.25-alpine-dev AS build WORKDIR /src COPY app/go.mod app/go.sum ./ RUN go mod download COPY app/ . RUN CGO_ENABLED=0 go build -o /out/app . # Stage 2: Minimal runtime image FROM dhi.io/static:20260611-alpine3.24 COPY --from=build /out/app /app COPY frontend/game /frontend ENTRYPOINT [\u0026#34;/app\u0026#34;] The Go compiler, package manager, and build tools stay in the build stage. The final production image contains only the statically compiled binary and the game assets. This approach reduces image size and eliminates unnecessary packages that could contain vulnerabilities.\n3. Docker Hardened Images (DHI) Both base images pull from dhi.io, Docker\u0026rsquo;s registry for security-hardened container images. These images ship minimal package footprints with fixed CVE patches.\nUsing hardened images occasionally reveals configuration differences compared to standard upstream images. For example, dhi.io/redis enables Redis protected-mode by default. In standard Docker networking across multiple containers, protected mode rejects connections originating outside 127.0.0.1. The project addresses this directly in docker-compose.yml:\nredis: image: dhi.io/redis:7.4-alpine3.24 command: [\u0026#34;redis-server\u0026#34;, \u0026#34;--protected-mode\u0026#34;, \u0026#34;no\u0026#34;] Explicitly overriding this setting allows the Go backend and microservices to communicate over the internal bridge network while keeping the Redis port unexposed to the host.\n4. Integration Testing with Testcontainers-go Mocking database drivers in Go often hides subtle runtime bugs. A mock cannot accurately replicate Redis TTL expiration windows, sorted set ranking nuances, or Redis Stream consumer group behaviour.\nCrossy Whale uses Testcontainers-go in app/internal/gate/window_test.go and scores-service/internal/scores/store_test.go to run tests against genuine, ephemeral Redis instances:\nfunc newTestRedisStore(t *testing.T) (*RedisWindowStore, func()) { ctx := context.Background() redisContainer, err := tcredis.Run(ctx, \u0026#34;dhi.io/redis:7.4-alpine3.24\u0026#34;) if err != nil { t.Fatalf(\u0026#34;failed to start redis container: %s\u0026#34;, err) } endpoint, err := redisContainer.ConnectionString(ctx) if err != nil { t.Fatalf(\u0026#34;failed to get connection string: %s\u0026#34;, err) } client := redis.NewClient(\u0026amp;redis.Options{Addr: endpoint}) store := NewRedisWindowStore(client) cleanup := func() { _ = redisContainer.Terminate(ctx) } return store, cleanup } When you execute go test ./..., Testcontainers calls the local Docker daemon, pulls the image if necessary, binds a random free port, runs the assertions, and terminates the container. Fast in-memory fakes remain available for pure unit tests, while Testcontainers validates real database interaction.\n5. Development Environments with Dev Containers To avoid version mismatches across different contributor machines, the repository includes a full dev container configuration in .devcontainer/devcontainer.json.\n{ \u0026#34;name\u0026#34;: \u0026#34;Crossy Whale Dev\u0026#34;, \u0026#34;image\u0026#34;: \u0026#34;mcr.microsoft.com/devcontainers/base:ubuntu-24.04\u0026#34;, \u0026#34;features\u0026#34;: { \u0026#34;ghcr.io/devcontainers/features/go:1\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;1.25\u0026#34; }, \u0026#34;ghcr.io/devcontainers/features/docker-outside-of-docker:1\u0026#34;: {} }, \u0026#34;customizations\u0026#34;: { \u0026#34;vscode\u0026#34;: { \u0026#34;extensions\u0026#34;: [ \u0026#34;golang.go\u0026#34;, \u0026#34;ms-azuretools.vscode-docker\u0026#34; ], \u0026#34;settings\u0026#34;: { \u0026#34;editor.formatOnSave\u0026#34;: true } } } } Two features stand out:\nPinned Go Toolchain: The container guarantees Go 1.25 matching the version declared in go.mod. Docker-outside-of-Docker: Mounting /var/run/docker.sock allows commands executed inside VS Code or the dev container terminal to manage containers on the host engine directly. Opening the folder in VS Code and selecting Reopen in Container initialises a ready-to-code workspace with all linters, extensions, and runtime tools installed.\n6. Local Kubernetes Deployment via Helm Transitioning from local development to container orchestration is covered in k8s/. The project packages the application services into a Helm chart designed for local clusters, including Docker Desktop\u0026rsquo;s built-in Kubernetes.\nThe chart defines:\nDeployments \u0026amp; Pods: Replicated Go microservices and Nginx ingress. ConfigMaps \u0026amp; Secrets: Injecting runtime variables cleanly. Services: Internal ClusterIP routing mirroring the Compose DNS architecture. You can spin up the full cluster deployment locally with:\nhelm install crossy-whale ./k8s Architectural Patterns to Inspect Beyond standard Docker features, several architectural details in the codebase are worth exploring:\nNginx Sub-Request Cookie Authentication When players finish a game, the client submits high scores via POST /api/leaderboard/scores. To prevent unauthorised spamming, Nginx intercepts write requests using auth_request:\nlocation = /api/leaderboard/scores { auth_request /internal/auth/validate-grant; proxy_pass http://scores_backend; } Nginx forwards the incoming cw_grant cookie to the Go backend for cryptographic verification before allowing the payload to reach the scores microservice. Unauthorised requests receive a 401 Unauthorized status at the proxy layer without hitting backend business logic.\nLive Leaderboard Streaming via SSE The leaderboard display (/leaderboard) uses Server-Sent Events (SSE) to update rankings in real time. When a new score is written, the scores service pushes an event down open HTTP streams, triggering instant DOM updates on presenter screens.\nLive Page Refresh on Redeploy The frontend polls /api/ping every two seconds. The endpoint returns a nanosecond timestamp captured during Go process initialisation:\n{ \u0026#34;id\u0026#34;: \u0026#34;1783513264497369178\u0026#34; } The browser tracks the initial ID. If a container redeploy changes the startup timestamp, the browser calls location.reload() automatically to load new assets.\nSandboxed AI Development with Claude Code The repository also includes tooling for AI coding agents inside isolated Docker Sandboxes (sbx). Running ./bin/onboard verifies local requirements, while ./bin/claude launches Claude Code inside a disposable microVM with pre-configured Model Context Protocol (MCP) servers for GitHub and documentation lookups.\nHost credentials are forwarded safely through sbx secret set, isolating agent execution from the host filesystem.\nRunning the Project To clone and explore the project locally:\n# Authenticate against Docker Hardened Images docker login dhi.io # Clone the repository git clone https://github.com/krisfoster/crossy-whale.git cd crossy-whale # Launch the application docker compose up -d Once running, navigate to:\nGame client: http://localhost/ (or http://localhost/play-local for direct local play) Presenter QR screen: http://localhost/host Live leaderboard: http://localhost/leaderboard Inspect docker-compose.yml and the associated service directories to see how these container patterns fit together in production code.\n","permalink":"/crossy-whale/","summary":"\u003cp\u003eMost container tutorials isolate individual tools. You read one guide on writing multi-stage Dockerfiles, another on setting up Compose networking, and a third on integration testing. Seeing how these pieces integrate into a cohesive, production-grade architecture is much rarer.\u003c/p\u003e\n\u003cp\u003eThe open-source repository \u003ca href=\"https://github.com/krisfoster/crossy-whale\"\u003ekrisfoster/crossy-whale\u003c/a\u003e demonstrates this more complete workflow through a playable demo. It is a browser-based arcade game starring the Docker whale navigating lanes of container traffic, built in Go and vanilla JavaScript. Beneath the game sits an architecture showcasing six core container technologies in a single repository.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Crossy Whale gameplay demo\" loading=\"lazy\" src=\"/images/container-obstacles.gif\"\u003e\u003c/p\u003e\n\u003ch2 id=\"1-multi-service-orchestration-with-docker-compose\"\u003e1. Multi-Service Orchestration with Docker Compose\u003c/h2\u003e\n\u003cp\u003eRunning the entire stack locally requires one command:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker compose up -d\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOpen \u003ca href=\"https://github.com/krisfoster/crossy-whale/blob/main/docker-compose.yml\"\u003e\u003ccode\u003edocker-compose.yml\u003c/code\u003e\u003c/a\u003e. Compose coordinates five core services on a shared private bridge network:\u003c/p\u003e","title":"A Tour of the Docker Ecosystem in One Demo"},{"content":"Running AI coding agents inside disposable, network-isolated containers protects your host environment from unintended commands and untrusted dependencies. Docker Sandbox (sbx), Docker\u0026rsquo;s CLI tool for running coding agents inside isolated microVMs, enforces strict default-deny egress policies. However, authenticating Claude Code against AWS Bedrock inside sbx introduces specific credential challenges that standard bearer token proxying, the default for sbx, cannot handle.\nThe companion repository krisfoster/claude-code-bedrock-sbx demonstrates two working architectures for connecting Claude Code to AWS Bedrock via AWS SSO: using the built-in claude-bedrock agent kit, or routing requests through a host-side LiteLLM proxy gateway.\nWhy Bedrock Breaks Standard Sandbox Secrets Most AI providers authenticate requests with static bearer tokens sent in an HTTP authorization header. In that scenario, sbx keeps real API keys on the host machine. The sandbox guest container receives a placeholder string. When the agent issues an outbound request, the sbx host proxy intercepts the traffic, strips the placeholder, and injects the genuine secret before forwarding the call upstream. Real credentials never enter the microVM.\nAWS Bedrock operates differently. It uses AWS Signature Version 4 (SigV4), which computes an HMAC-SHA256 cryptographic signature across the HTTP method, URI, request headers, timestamp, and body payload. Because the client library computes this signature prior to transmission using your secret access key, a host-side proxy cannot transparently swap in credentials after the fact. If the container holds an invalid placeholder, Bedrock rejects the request with SignatureDoesNotMatch or UnrecognizedClientException.\nAWS SSO adds another layer of complexity. Instead of issuing permanent access keys, IAM Identity Center returns temporary Security Token Service (STS) credentials containing a session token. These credentials expire within hours and require ongoing refreshing.\nTo run Claude Code inside sbx, you must choose between two strategies:\nInject short-lived STS credentials directly into the microVM and refresh them continuously. Terminate SigV4 on the host via a LiteLLM gateway, exposing a standard bearer-token endpoint to the sandbox. Shared Environment Setup Both approaches share common baseline settings: an AWS SSO profile, target region, and inference profile.\nEnvironment Management with direnv To prevent hardcoding configurations across multiple scripts, place non-secret environment variables in a root .env file managed by direnv:\n# .env PROFILE=bedrock REGION=us-east-1 GEO=us HAIKU=us.anthropic.claude-haiku-4-5-20251001-v1:0 Bedrock uses cross-region inference profiles to route requests across multiple AWS data centres in a region group, improving availability and throughput. For newer models like Claude Haiku 4.5, Bedrock mandates this prefixed identifier (us.anthropic...) rather than a direct model ID. The geographical prefix (such as us. or eu.) must match the destination AWS region.\nAllow direnv in your project folder:\ndirenv allow AWS SSO Profile Initialisation Configure your host AWS CLI with an SSO profile:\naws configure sso --profile bedrock aws sso login --profile bedrock aws sts get-caller-identity --profile bedrock The SSO start URL is your organization access portal URL from IAM Identity Center. All authentication prompts execute on your host machine; the sandbox never runs interactive browser logins.\nPattern 1: The Native claude-bedrock Kit The built-in claude-bedrock agent handles credential lifecycle management natively. On the host, sbx resolves your SSO profile into short-lived STS credentials, passes them into the sandbox container environment at runtime, and refreshes them before expiration.\n1. Register the Profile Store your SSO profile identifier in the sbx keychain once:\necho \u0026#34;$PROFILE\u0026#34; | sbx secret set bedrock 2. Configure Egress Policy In sbx, a kit is a declarative YAML manifest that applies custom network rules, mounts, or environment variables to a sandbox. Because sbx defaults to denying all outbound traffic, create a mixin specification (bedrock-kit/spec.yaml) that explicitly permits Bedrock endpoints:\nschemaVersion: \u0026#34;1\u0026#34; kind: mixin name: bedrock-haiku network: allowedDomains: - bedrock-runtime.us-east-1.amazonaws.com:443 - bedrock.us-east-1.amazonaws.com:443 environment: variables: CLAUDE_CODE_USE_BEDROCK: \u0026#34;1\u0026#34; Validate the kit:\nsbx kit validate ./bedrock-kit 3. Pin Model Settings Create a directory-level configuration file at .claude/settings.json to instruct Claude Code to use your provisioned Haiku inference profile:\n{ \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_MODEL\u0026#34;: \u0026#34;us.anthropic.claude-haiku-4-5-20251001-v1:0\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_HAIKU_MODEL\u0026#34;: \u0026#34;us.anthropic.claude-haiku-4-5-20251001-v1:0\u0026#34; } } 4. Launch the Sandbox Run Claude Code with the mixin kit applied to the workspace:\nsbx run --kit ./bedrock-kit claude-bedrock . Inside the active session, running /status confirms that Claude Code is operating in Bedrock mode.\nPattern 2: Host-Side LiteLLM Gateway The LiteLLM pattern completely isolates AWS credentials from the guest environment. A lightweight LiteLLM container runs on the host, mounts ~/.aws read-only, and performs SigV4 signing on behalf of the sandbox. The sandbox interacts with a standard Anthropic-compatible /v1/messages endpoint using a simple API token.\n1. Compose Configuration Define the gateway in a docker-compose.yml file using an inline configuration block. The configuration defines LITELLM_MASTER_KEY (the bearer token clients use to query the gateway) and LITELLM_SALT_KEY (used internally to encrypt stored credentials):\nservices: litellm: image: ghcr.io/berriai/litellm:main-latest ports: - \u0026#34;127.0.0.1:4000:4000\u0026#34; volumes: - ~/.aws:/root/.aws:ro environment: AWS_PROFILE: ${PROFILE} AWS_REGION_NAME: ${REGION} LITELLM_MASTER_KEY: sk-1234 LITELLM_SALT_KEY: sk-REPLACE-ME configs: - source: litellm_config target: /app/config.yaml configs: litellm_config: content: | model_list: - model_name: bedrock-claude-haiku litellm_params: model: bedrock/${HAIKU} aws_region_name: ${REGION} aws_profile_name: ${PROFILE} Start the proxy on your host:\ndocker compose up -d Verify that LiteLLM can invoke Bedrock over its local HTTP port:\ncurl -sS http://localhost:4000/v1/messages \\ -H \u0026#34;content-type: application/json\u0026#34; \\ -H \u0026#34;x-api-key: sk-1234\u0026#34; \\ -H \u0026#34;anthropic-version: 2023-06-01\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;bedrock-claude-haiku\u0026#34;, \u0026#34;max_tokens\u0026#34;: 64, \u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Reply with: ok\u0026#34;}] }\u0026#39; 2. Network Policy and Proxy Rewriting When configuring network access from sbx to the host LiteLLM container, domain resolution requires specific care. Claude Code inside the container targets http://host.docker.internal:4000. However, the sbx egress proxy normalises host.docker.internal:\u0026lt;port\u0026gt; to localhost:\u0026lt;port\u0026gt; before evaluating network policy rules.\nBecause of this normalisation, your mixin kit (litellm-kit/spec.yaml) must allow localhost:4000 rather than host.docker.internal:4000:\nschemaVersion: \u0026#34;1\u0026#34; kind: mixin name: litellm-gateway network: allowedDomains: - localhost:4000 If you specify host.docker.internal:4000 in allowedDomains, the rule check fails and the proxy blocks the request with Blocked by network policy: domain localhost:4000.\n3. Configure Claude Code Point Claude Code at the host gateway in .claude/settings.json:\n{ \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;http://host.docker.internal:4000\u0026#34;, \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;sk-1234\u0026#34;, \u0026#34;ANTHROPIC_MODEL\u0026#34;: \u0026#34;bedrock-claude-haiku\u0026#34; } } Setting ANTHROPIC_AUTH_TOKEN passes the key via the Authorization: Bearer header. This avoids triggering sbx\u0026rsquo;s automated secret injector, which specifically watches for the ANTHROPIC_API_KEY environment variable to substitute placeholder values. Do not set CLAUDE_CODE_USE_BEDROCK here, as the agent communicates with an Anthropic-compatible gateway rather than the Bedrock API directly. The ANTHROPIC_AUTH_TOKEN value must match LITELLM_MASTER_KEY in your Compose file, otherwise the gateway returns HTTP 401 Unauthorized.\nLaunch the sandbox using the standard claude agent (rather than claude-bedrock):\nsbx run --kit ./litellm-kit claude . To verify connectivity inside the microVM, query LiteLLM\u0026rsquo;s liveliness endpoint:\ncurl -sS http://host.docker.internal:4000/health/liveliness Architecture Comparison Dimension Native claude-bedrock Kit LiteLLM Gateway Agent Target Experimental claude-bedrock agent Standard claude agent Credential Boundary Short-lived STS tokens injected into VM AWS keys remain strictly on the host Infrastructure Overhead None, uses built-in sbx features Requires a local Docker Compose service Agent Flexibility Bound to Claude Code agent Works with any agent supporting OpenAI or Anthropic APIs SSO Expiration Host CLI refresh propagates to sandbox Run aws sso login and restart Compose container Summary When working with AWS Bedrock in restricted developer sandboxes, SigV4 requirements prevent conventional outbound token replacement. The native claude-bedrock kit provides a direct path by managing short-lived STS tokens inside the guest VM. Alternatively, the host LiteLLM gateway provides stronger security boundaries by keeping all AWS signing outside the microVM entirely.\nComplete configuration files, kits, and Compose recipes are available in the repository at github.com/krisfoster/claude-code-bedrock-sbx.\n","permalink":"/claude-code-bedrock-sbx/","summary":"\u003cp\u003eRunning AI coding agents inside disposable, network-isolated containers protects your host environment from unintended commands and untrusted dependencies. Docker Sandbox (\u003ccode\u003esbx\u003c/code\u003e), Docker\u0026rsquo;s CLI tool for running coding agents inside isolated microVMs, enforces strict default-deny egress policies. However, authenticating Claude Code against AWS Bedrock inside \u003ccode\u003esbx\u003c/code\u003e introduces specific credential challenges that standard bearer token proxying, the default for \u003ccode\u003esbx\u003c/code\u003e, cannot handle.\u003c/p\u003e\n\u003cp\u003eThe companion repository \u003ca href=\"https://github.com/krisfoster/claude-code-bedrock-sbx\"\u003ekrisfoster/claude-code-bedrock-sbx\u003c/a\u003e demonstrates two working architectures for connecting Claude Code to AWS Bedrock via AWS SSO: using the built-in \u003ccode\u003eclaude-bedrock\u003c/code\u003e agent kit, or routing requests through a host-side LiteLLM proxy gateway.\u003c/p\u003e\n\u003ch2 id=\"why-bedrock-breaks-standard-sandbox-secrets\"\u003eWhy Bedrock Breaks Standard Sandbox Secrets\u003c/h2\u003e\n\u003cp\u003eMost AI providers authenticate requests with static bearer tokens sent in an HTTP authorization header. In that scenario, \u003ccode\u003esbx\u003c/code\u003e keeps real API keys on the host machine. The sandbox guest container receives a placeholder string. When the agent issues an outbound request, the \u003ccode\u003esbx\u003c/code\u003e host proxy intercepts the traffic, strips the placeholder, and injects the genuine secret before forwarding the call upstream. Real credentials never enter the microVM.\u003c/p\u003e","title":"Running Claude Code on AWS Bedrock Inside Docker Sandbox"},{"content":"Working offline, navigating corporate proxies that intercept TLS, or verifying whether a mirror configuration resolves correctly requires a container registry that you control. Docker provides an official container image for this, registry:2, which spins up a working registry with a single command: docker run -d -p 5000:5000 registry:2. However, running the container naively stores everything inside its writable layer, wiping your cached images the moment the container is removed. On top of that, standard docker tag behaviour makes it easy to accidentally overwrite images when pushing multiple versions.\nTo solve these issues, I wrapped registry:2 in a small shell script, local-docker-registry. Here is how to handle persistence, avoid image clobbering, query the underlying HTTP API, and catch configuration drift.\nPersistence and Container Lifecycle Running the official registry image locally is straightforward:\ndocker run -d -p 5000:5000 --name registry registry:2 The Docker daemon treats loopback addresses (localhost and 127.0.0.1) as insecure by default, allowing pushes and pulls without TLS or daemon configuration changes. However, container storage is ephemeral; removing the container with docker rm deletes all pushed image layers and manifests.\nTo persist images across container removals and reboots, bind-mount host storage to /var/lib/registry (the internal directory where registry:2 stores all layer blobs and manifests):\ndocker run -d \\ -p 5000:5000 \\ --restart=always \\ --name registry \\ -v \u0026#34;$HOME/.local-docker-registry/data:/var/lib/registry\u0026#34; \\ registry:2 With storage bound to the host, the container can be recreated or upgraded at any time without data loss.\nTag Retention and Image Clobbering When tagging an image to point to a local registry, omitting the tag defaults to latest:\ndocker tag ubuntu:24.04 localhost:5000/ubuntu The source tag (24.04) is discarded, and the target image becomes localhost:5000/ubuntu:latest. Tagging multiple versions of the same repository without explicitly re-specifying the tag overwrites the previous version:\ndocker tag python:3.12-slim localhost:5000/python # stored as :latest docker tag python:3.11-slim localhost:5000/python # overwrites :latest The earlier image layer becomes untagged and inaccessible via the registry tag index.\nTo prevent this, local-registry add parses the image reference and retains the source tag automatically. When adding an image pinned by an immutable digest (such as ubuntu@sha256:754cc6...) rather than a tag, Docker cannot push the digest directly as a destination tag name. The script extracts the first twelve characters of the SHA-256 hash to generate a deterministic tag:\nparse_ref() { local ref=\u0026#34;$1\u0026#34; last name_part digest_part digest_hex last=\u0026#34;${ref##*/}\u0026#34; if [[ \u0026#34;$last\u0026#34; == *@* ]]; then name_part=\u0026#34;${last%%@*}\u0026#34; if [[ \u0026#34;$name_part\u0026#34; == *:* ]]; then PARSED_REPO=\u0026#34;${name_part%%:*}\u0026#34; PARSED_TAG=\u0026#34;${name_part##*:}\u0026#34; else digest_part=\u0026#34;${last#*@}\u0026#34; # e.g. sha256:abcd1234... digest_hex=\u0026#34;${digest_part#*:}\u0026#34; PARSED_REPO=\u0026#34;$name_part\u0026#34; PARSED_TAG=\u0026#34;${digest_hex:0:12}\u0026#34; fi elif [[ \u0026#34;$last\u0026#34; == *:* ]]; then ... This ensures pulling, tagging, and pushing can be performed in one operation while preserving identifiers:\nlocal-registry add ubuntu:24.04 local-registry add ubuntu:24.04 as my-ubuntu # localhost:5000/my-ubuntu:24.04 local-registry add ubuntu:24.04 as my-ubuntu:custom # localhost:5000/my-ubuntu:custom Interacting with the Registry HTTP API The Docker registry implements the OCI Distribution Specification over HTTP, which defines the standard /v2/ REST API that container engines use to fetch manifests and layer blobs. You can inspect the contents of a local registry using standard tools like curl.\nTo list all repositories in the registry:\ncurl -s http://localhost:5000/v2/_catalog {\u0026#34;repositories\u0026#34;:[\u0026#34;alpine\u0026#34;,\u0026#34;nginx\u0026#34;,\u0026#34;python\u0026#34;,\u0026#34;redis\u0026#34;,\u0026#34;ubuntu\u0026#34;]} To list all tags for a specific repository:\ncurl -s http://localhost:5000/v2/python/tags/list {\u0026#34;name\u0026#34;:\u0026#34;python\u0026#34;,\u0026#34;tags\u0026#34;:[\u0026#34;3.11-slim\u0026#34;,\u0026#34;3.12-slim\u0026#34;]} local-registry list queries these endpoints and formats the output into a table:\nREPOSITORY TAGS alpine 3.20 python 3.12-slim, 3.11-slim To keep the wrapper portable across minimal environments without requiring jq, catalog responses are parsed directly with sed:\n# Extracts the quoted string elements of a single JSON array field, e.g. # \u0026#39;{\u0026#34;repositories\u0026#34;:[\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;]}\u0026#39; -\u0026gt; \u0026#34;a\\nb\u0026#34;. Good enough for the registry API\u0026#39;s # always-flat repositories/tags arrays; avoids a hard dependency on jq. json_array_field() { local json=\u0026#34;$1\u0026#34; field=\u0026#34;$2\u0026#34; echo \u0026#34;$json\u0026#34; | sed -n \u0026#34;s/.*\\\u0026#34;${field}\\\u0026#34;:\\[\\(.*\\)\\].*/\\1/p\u0026#34; | tr -d \u0026#39;\u0026#34;\u0026#39; | tr \u0026#39;,\u0026#39; \u0026#39;\\n\u0026#39; | sed \u0026#39;/^$/d\u0026#39; } Because the registry\u0026rsquo;s catalog and tag list endpoints return single-level string arrays, basic stream editing extracts the values reliably without external dependencies.\nDetecting Configuration Drift When wrapping Docker commands, passing new environment variables to docker start does not update an existing container:\nREGISTRY_PORT=6000 local-registry start Port bindings, volume mounts, and network settings are fixed at container creation. If a container named registry already exists on port 5000, subsequent start commands reuse the original port mapping.\nTo prevent silent misconfigurations, the script inspects the running container and compares the actual port with the requested value:\nexisting_host_port() { docker inspect --format \u0026#39;{{ (index (index .NetworkSettings.Ports \u0026#34;5000/tcp\u0026#34;) 0).HostPort }}\u0026#39; \\ \u0026#34;$CONTAINER_NAME\u0026#34; 2\u0026gt;/dev/null } If the active configuration differs from the requested environment variables, the script warns the user and provides the local-registry recreate command to rebuild the container with the updated settings while leaving host data intact.\nBash Scripting Considerations Two specific Bash behaviours require explicit handling in the wrapper.\nset -e in Conditionals In Bash, errexit (set -e) is disabled within conditional statements, including commands placed on the left side of \u0026amp;\u0026amp; or inside if tests. When the batch import command (local-registry load, which mirrors an image list from a file) invokes cmd_add within a conditional check:\ncmd_add \u0026#34;$src\u0026#34; as \u0026#34;$dest_name\u0026#34; \u0026amp;\u0026amp; ok=1 || ok=0 set -e remains inactive inside cmd_add. If a command fails during the function execution (such as a network failure during docker pull), execution does not abort automatically. Each critical step requires explicit status checking:\ndocker pull \u0026#34;$src\u0026#34; || { echo \u0026#34;error: failed to pull $src\u0026#34; \u0026gt;\u0026amp;2; return 1; } Batch Error Handling When mirroring a list of images with local-registry load, individual pull failures should not abort the entire batch. The script logs individual errors, continues processing subsequent images, and reports a final status summary:\nLoaded 18 image(s), 1 failed. The script exits with a non-zero status code after the batch completes so automated CI pipelines detect partial failures.\nScope and Limitations The script is scoped specifically for local development and offline mirror testing:\nNo TLS or Authentication: Intended solely for loopback interfaces (localhost), which Docker permits without certificates. Network-accessible registries require TLS and credential management. Push-Populated Registry: The default setup acts as an explicit storage target rather than a transparent pull-through cache (proxy.remoteurl), which automatically fetches and caches missing upstream images from Docker Hub on demand. Storage Reclaiming: Deleting an image tag via the HTTP API only removes the manifest reference; underlying layer blobs remain on disk until running registry garbage-collect. The script provides purge to remove the container and host data directory entirely, requiring interactive confirmation or the --yes flag in scripts: if [ ! -t 0 ]; then echo \u0026#34;error: refusing to purge without confirmation in a non-interactive shell; re-run with --yes\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi Port 5000 on macOS: macOS AirPlay Receiver listens on port 5000 by default. Set REGISTRY_PORT to an alternate port (such as 5001) or disable AirPlay Receiver in system settings. Summary Running registry:2 locally provides a reliable, isolated target for testing container builds, offline workflows, and mirror resolution. Using persistent host volume mounts protects image layers across reboots, while automated tag handling prevents unintended tag overwrites.\nThe complete script is available at github.com/krisfoster/local-docker-registry.\n","permalink":"/local-docker-registry/","summary":"\u003cp\u003eWorking offline, navigating corporate proxies that intercept TLS, or verifying whether a mirror configuration resolves correctly requires a container registry that you control. Docker provides an official container image for this, \u003ccode\u003eregistry:2\u003c/code\u003e, which spins up a working registry with a single command: \u003ccode\u003edocker run -d -p 5000:5000 registry:2\u003c/code\u003e. However, running the container naively stores everything inside its writable layer, wiping your cached images the moment the container is removed. On top of that, standard \u003ccode\u003edocker tag\u003c/code\u003e behaviour makes it easy to accidentally overwrite images when pushing multiple versions.\u003c/p\u003e\n\u003cp\u003eTo solve these issues, I wrapped \u003ccode\u003eregistry:2\u003c/code\u003e in a small shell script, \u003ca href=\"https://github.com/krisfoster/local-docker-registry\"\u003elocal-docker-registry\u003c/a\u003e. Here is how to handle persistence, avoid image clobbering, query the underlying HTTP API, and catch configuration drift.\u003c/p\u003e\n\u003ch2 id=\"persistence-and-container-lifecycle\"\u003ePersistence and Container Lifecycle\u003c/h2\u003e\n\u003cp\u003eRunning the official registry image locally is straightforward:\u003c/p\u003e","title":"A Local Docker Registry That Survives a Reboot"}]