Skip to content

Commit 444604a

Browse files
committed
Add devcontainer support for Codespaces, local, and Claude Code
Adds .devcontainer/devcontainer.json (docker-compose based, reusing the existing dev target/services) with: - git, github-cli, and common-utils features for a usable shell/terminal - the official Claude Code devcontainer feature, with CLAUDE_CONFIG_DIR pointed at a persisted volume so login survives rebuilds - docker-outside-of-docker so `docker compose logs` works from the integrated terminal (requires moby:false, since moby packages aren't published for Debian trixie) - forwarded ports for the app, Sentry Spotlight, and Storybook Removes the old root-level devcontainer.json, which pointed at a nonexistent workspace path and predates the .devcontainer/ convention. Requires the Dockerfile syntax bump to 1.4: devcontainer feature injection uses BuildKit named build contexts, which aren't supported under 1.2. Also bakes Playwright's browsers into the dev image (otherwise lost every time the devcontainer is recreated) so `npm run test:e2e` and the Storybook vitest addon work out of the box, pins the compose port mappings to 0.0.0.0 (Docker's default dual-stack publish was confusing VS Code's port forwarding into offering a non-working [::1] address), and allow-lists localhost/127.0.0.1 as Next.js dev origins for the same forwarded-port reason. Fixes an unrelated Storybook glob that never matched any files (all stories are .tsx; the plain .[jt]s pattern only matches .js/.ts) and was printing a spurious "no story files found" warning on every start.
1 parent fdb523a commit 444604a

12 files changed

Lines changed: 176 additions & 32 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"features": {
3+
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {
4+
"version": "1.0.5",
5+
"resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a",
6+
"integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a"
7+
},
8+
"ghcr.io/devcontainers/features/common-utils:2": {
9+
"version": "2.5.9",
10+
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
11+
"integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a"
12+
},
13+
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
14+
"version": "1.10.0",
15+
"resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e",
16+
"integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e"
17+
},
18+
"ghcr.io/devcontainers/features/git:1": {
19+
"version": "1.3.8",
20+
"resolved": "ghcr.io/devcontainers/features/git@sha256:fd75977de13a9979000e0e78baf949adb0ca71d2398995fa22e0a36d7e7e7fe2",
21+
"integrity": "sha256:fd75977de13a9979000e0e78baf949adb0ca71d2398995fa22e0a36d7e7e7fe2"
22+
},
23+
"ghcr.io/devcontainers/features/github-cli:1": {
24+
"version": "1.1.0",
25+
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
26+
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
27+
}
28+
}
29+
}

.devcontainer/devcontainer.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "Mariner's Dashboard",
3+
"dockerComposeFile": ["../docker-compose.yaml", "../docker-compose.dev.yaml"],
4+
"service": "client",
5+
"runServices": ["client", "spotlight"],
6+
"workspaceFolder": "/app",
7+
"overrideCommand": false,
8+
"shutdownAction": "stopCompose",
9+
"features": {
10+
"ghcr.io/devcontainers/features/git:1": {},
11+
"ghcr.io/devcontainers/features/github-cli:1": {},
12+
"ghcr.io/devcontainers/features/common-utils:2": {
13+
"installOhMyZsh": true,
14+
"configureZshAsDefaultShell": true
15+
},
16+
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
17+
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
18+
"moby": false
19+
}
20+
},
21+
"forwardPorts": [3000, 8969, 6006],
22+
"portsAttributes": {
23+
"3000": {
24+
"label": "Next.js app",
25+
"onAutoForward": "openBrowser"
26+
},
27+
"8969": {
28+
"label": "Sentry Spotlight",
29+
"onAutoForward": "silent"
30+
},
31+
"6006": {
32+
"label": "Storybook",
33+
"onAutoForward": "silent"
34+
}
35+
},
36+
"mounts": [
37+
"source=neracoos-buoy-app-claude-config,target=/root/.claude,type=volume",
38+
"source=neracoos-buoy-app-gh-config,target=/root/.config/gh,type=volume"
39+
],
40+
"containerEnv": {
41+
"CLAUDE_CONFIG_DIR": "/root/.claude"
42+
},
43+
"postCreateCommand": "git config --global --add safe.directory /app",
44+
"customizations": {
45+
"vscode": {
46+
"settings": {
47+
"git.enabled": true,
48+
"scm.diffDecorations": "all"
49+
},
50+
"extensions": ["vitest.explorer", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "editorconfig.editorconfig"]
51+
}
52+
}
53+
}

.storybook/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { StorybookConfig } from "@storybook/nextjs-vite"
22

33
const config: StorybookConfig = {
4-
stories: ["../src/**/*.stories.[jt]s", "../src/**/*.stories.[jt]sx", "../app/**/*.stories.[jt]sx"],
4+
stories: ["../src/**/*.stories.[jt]sx", "../app/**/*.stories.[jt]sx"],
55

66
addons: ["@storybook/addon-a11y", "@storybook/addon-links", "@storybook/addon-docs", "@storybook/addon-vitest"],
77

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#syntax=docker/dockerfile:1.2
1+
#syntax=docker/dockerfile:1.4
22
FROM node:26.5.0-slim@sha256:715e55e4b84e4bb0ff48e49b398a848f08e55daed8eb6a0ea1839ae53bc57583 AS base
33

44
# Install dependencies only when needed
@@ -17,6 +17,11 @@ WORKDIR /app
1717
COPY --from=deps /app/node_modules ./node_modules
1818
COPY . .
1919

20+
# Playwright (e2e tests) and the Storybook vitest addon both need real browsers,
21+
# which aren't part of node_modules. Install them here so they're baked into the
22+
# dev image rather than lost every time the devcontainer is recreated.
23+
RUN npx playwright install --with-deps chromium firefox
24+
2025
# Rebuild the source code only when needed
2126
FROM base AS builder
2227

devcontainer.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

docker-compose.dev.yaml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ services:
88
target: dev
99
command: npm run dev
1010
stdin_open: true
11+
ports:
12+
- "0.0.0.0:6006:6006"
1113
volumes:
1214
- .:/app:cached
1315
- /app/node_modules/
@@ -16,12 +18,16 @@ services:
1618

1719
environment:
1820
NEXT_PUBLIC_ERDDAP_SERVICE: https://buoybarn.neracoos.org
21+
# The server runs inside the compose network, so it reaches Spotlight via
22+
# the "spotlight" service hostname directly.
1923
SENTRY_SPOTLIGHT: http://spotlight:8969/stream
20-
NEXT_PUBLIC_SENTRY_SPOTLIGHT: http://spotlight:8969/stream
21-
NEXT_PUBLIC_SENTRY_DSN: http://spotlight@spotlight:8969/0
22-
SENTRY_DSN: http://spotlight@spotlight:8969/0
24+
# The browser can't resolve "spotlight" (it isn't on the compose network,
25+
# and in Codespaces it may not even be on the same machine), so the client
26+
# only gets an enable flag here; instrumentation-client.ts works out the
27+
# right sidecar URL from the page's own origin at runtime.
28+
NEXT_PUBLIC_SENTRY_SPOTLIGHT: "1"
2329

2430
spotlight:
2531
image: ghcr.io/getsentry/spotlight:latest
2632
ports:
27-
- "8969:8969"
33+
- "0.0.0.0:8969:8969"

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ services:
77
# command: "npm run dev"
88
stdin_open: true
99
ports:
10-
- "3000:3000"
10+
- "0.0.0.0:3000:3000"
1111
environment:
1212
NEXT_PUBLIC_ERDDAP_SERVICE: https://buoybarn.neracoos.org
1313
# NEXT_PUBLIC_ERDDAP_SERVICE: http://localhost:8080

next.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const nextConfig = {
1111
silenceDeprecations: ["color-functions", "global-builtin", "import", "if-function"],
1212
},
1313
serverExternalPackages: ["web-worker"],
14+
// Devcontainer/Docker forward ports through 127.0.0.1 and localhost rather than
15+
// the container's own hostname, which Next.js's dev server otherwise blocks as cross-origin.
16+
allowedDevOrigins: ["localhost", "127.0.0.1"],
1417
experimental: {
1518
craCompat: false,
1619
},

src/instrumentation-client.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ import * as Sentry from "@sentry/nextjs"
1111
// tslint:disable-next-line:no-var-requires
1212
const packageJson = require("../package.json")
1313

14+
/**
15+
* The browser can't resolve the "spotlight" Docker service hostname, and in
16+
* forwarded-port dev environments (e.g. GitHub Codespaces) it may not even be
17+
* on the same machine. Forwarded-port hosts expose each port under a
18+
* "<name>-<port>.<domain>" hostname, so derive Spotlight's URL from whatever
19+
* origin the page is actually being viewed from. Returns undefined when the
20+
* current hostname doesn't match that pattern (e.g. local Docker Desktop),
21+
* letting the SDK fall back to its "http://localhost:8969/stream" default.
22+
*/
23+
function spotlightSidecarUrl() {
24+
const match = window.location.hostname.match(/^(.*)-\d+(\..+)$/)
25+
if (!match) return undefined
26+
return `${window.location.protocol}//${match[1]}-8969${match[2]}/stream`
27+
}
28+
1429
Sentry.init({
1530
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN ?? "__dsn__",
1631

@@ -35,6 +50,9 @@ Sentry.init({
3550
maskAllText: true,
3651
blockAllMedia: true,
3752
}),
53+
...(process.env.NEXT_PUBLIC_SENTRY_SPOTLIGHT
54+
? [Sentry.spotlightBrowserIntegration({ sidecarUrl: spotlightSidecarUrl() })]
55+
: []),
3856
],
3957
})
4058

src/instrumentation.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
import * as Sentry from "@sentry/nextjs"
22

3-
/**
4-
* Load our package.json so that we can access the version
5-
* and allow Sentry to track errors in relation to the version used
6-
*/
7-
// tslint:disable-next-line:no-var-requires
8-
const packageJson = require("../package.json")
9-
10-
export function register() {
11-
if (process.env.NEXT_RUNTIME === "nodejs" || process.env.NEXT_RUNTIME === "edge") {
12-
Sentry.init({
13-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN ?? "__dsn__",
14-
15-
release: `v${packageJson.version}`,
16-
17-
// Adjust this value in production, or use tracesSampler for greater control
18-
tracesSampleRate: process.env.NODE_ENV === "development" ? 1 : 0.05,
19-
20-
// Setting this option to true will print useful information to the console while you're setting up Sentry.
21-
debug: false,
22-
})
3+
// The server and edge runtimes get bundled separately; importing each config
4+
// only from its matching branch keeps runtime-specific SDK code (like
5+
// spotlightIntegration, which the edge build doesn't export) out of the
6+
// wrong bundle. See sentry.server.config.ts / sentry.edge.config.ts.
7+
export async function register() {
8+
if (process.env.NEXT_RUNTIME === "nodejs") {
9+
await import("./sentry.server.config")
10+
}
11+
if (process.env.NEXT_RUNTIME === "edge") {
12+
await import("./sentry.edge.config")
2313
}
2414
}
2515

0 commit comments

Comments
 (0)