Compare commits

..

2 Commits

Author SHA1 Message Date
blink-so[bot] 16361af95a docs: fix section header and remove inconsistent example 2026-01-14 07:49:07 +00:00
blink-so[bot] e8eef04afb docs: add Docker Compose devcontainers section to user guide 2026-01-13 17:48:17 +00:00
16 changed files with 621 additions and 752 deletions
-1
View File
@@ -402,7 +402,6 @@ func WorkspaceAgentDevcontainer(t testing.TB, db database.Store, orig database.W
Name: []string{takeFirst(orig.Name, testutil.GetRandomName(t))},
WorkspaceFolder: []string{takeFirst(orig.WorkspaceFolder, "/workspace")},
ConfigPath: []string{takeFirst(orig.ConfigPath, "")},
SubagentID: []uuid.UUID{takeFirst(orig.SubagentID, uuid.NullUUID{}).UUID},
})
require.NoError(t, err, "insert workspace agent devcontainer")
return devcontainers[0]
+1 -2
View File
@@ -2505,8 +2505,7 @@ CREATE TABLE workspace_agent_devcontainers (
created_at timestamp with time zone DEFAULT now() NOT NULL,
workspace_folder text NOT NULL,
config_path text NOT NULL,
name text NOT NULL,
subagent_id uuid
name text NOT NULL
);
COMMENT ON TABLE workspace_agent_devcontainers IS 'Workspace agent devcontainer configuration';
@@ -1 +1 @@
DROP INDEX IF EXISTS workspace_agents_auth_instance_id_deleted_idx;
DROP INDEX IF EXISTS public.workspace_agents_auth_instance_id_deleted_idx;
@@ -1 +1 @@
CREATE INDEX IF NOT EXISTS workspace_agents_auth_instance_id_deleted_idx ON workspace_agents (auth_instance_id, deleted);
CREATE INDEX IF NOT EXISTS workspace_agents_auth_instance_id_deleted_idx ON public.workspace_agents (auth_instance_id, deleted);
@@ -1,2 +0,0 @@
ALTER TABLE workspace_agent_devcontainers
DROP COLUMN subagent_id;
@@ -1,2 +0,0 @@
ALTER TABLE workspace_agent_devcontainers
ADD COLUMN subagent_id UUID;
+1 -2
View File
@@ -4743,8 +4743,7 @@ type WorkspaceAgentDevcontainer struct {
// Path to devcontainer.json.
ConfigPath string `db:"config_path" json:"config_path"`
// The name of the Dev Container.
Name string `db:"name" json:"name"`
SubagentID uuid.NullUUID `db:"subagent_id" json:"subagent_id"`
Name string `db:"name" json:"name"`
}
type WorkspaceAgentLog struct {
-96
View File
@@ -7989,99 +7989,3 @@ func TestDeleteExpiredAPIKeys(t *testing.T) {
require.NoError(t, err)
require.Len(t, remaining, len(unexpiredTimes))
}
func TestWorkspaceAgentDevcontainersSubagentID(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
// Setup: create workspace agent
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
tpl := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
CreatedBy: user.ID,
})
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
CreatedBy: user.ID,
})
ws := dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: tpl.ID,
})
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
})
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: ws.ID,
JobID: job.ID,
TemplateVersionID: tv.ID,
})
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
})
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: res.ID,
})
// Create a subagent that will be referenced
subagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: res.ID,
ParentID: uuid.NullUUID{UUID: agent.ID, Valid: true},
})
t.Run("InsertWithSubagentID", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
devcontainers, err := db.InsertWorkspaceAgentDevcontainers(ctx, database.InsertWorkspaceAgentDevcontainersParams{
WorkspaceAgentID: agent.ID,
CreatedAt: dbtime.Now(),
ID: []uuid.UUID{uuid.New()},
Name: []string{"test-devcontainer"},
WorkspaceFolder: []string{"/workspace"},
ConfigPath: []string{"/workspace/.devcontainer/devcontainer.json"},
SubagentID: []uuid.UUID{subagent.ID},
})
require.NoError(t, err)
require.Len(t, devcontainers, 1)
require.True(t, devcontainers[0].SubagentID.Valid)
require.Equal(t, subagent.ID, devcontainers[0].SubagentID.UUID)
// Verify retrieval
retrieved, err := db.GetWorkspaceAgentDevcontainersByAgentID(ctx, agent.ID)
require.NoError(t, err)
require.Len(t, retrieved, 1)
require.True(t, retrieved[0].SubagentID.Valid)
require.Equal(t, subagent.ID, retrieved[0].SubagentID.UUID)
})
t.Run("InsertWithNilSubagentID", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
// Create a separate agent for this subtest
agent2 := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: res.ID,
})
// When uuid.Nil is passed, it stores the zero UUID (not NULL).
// This matches the provisionerdserver behavior.
devcontainers, err := db.InsertWorkspaceAgentDevcontainers(ctx, database.InsertWorkspaceAgentDevcontainersParams{
WorkspaceAgentID: agent2.ID,
CreatedAt: dbtime.Now(),
ID: []uuid.UUID{uuid.New()},
Name: []string{"no-subagent"},
WorkspaceFolder: []string{"/workspace"},
ConfigPath: []string{""},
SubagentID: []uuid.UUID{uuid.Nil},
})
require.NoError(t, err)
require.Len(t, devcontainers, 1)
// uuid.Nil is stored as a zero UUID, not NULL.
require.Equal(t, uuid.Nil, devcontainers[0].SubagentID.UUID)
})
}
+4 -9
View File
@@ -17336,7 +17336,7 @@ func (q *sqlQuerier) ValidateUserIDs(ctx context.Context, userIds []uuid.UUID) (
const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
SELECT
id, workspace_agent_id, created_at, workspace_folder, config_path, name, subagent_id
id, workspace_agent_id, created_at, workspace_folder, config_path, name
FROM
workspace_agent_devcontainers
WHERE
@@ -17361,7 +17361,6 @@ func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context
&i.WorkspaceFolder,
&i.ConfigPath,
&i.Name,
&i.SubagentID,
); err != nil {
return nil, err
}
@@ -17378,16 +17377,15 @@ func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context
const insertWorkspaceAgentDevcontainers = `-- name: InsertWorkspaceAgentDevcontainers :many
INSERT INTO
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path, subagent_id)
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path)
SELECT
$1::uuid AS workspace_agent_id,
$2::timestamptz AS created_at,
unnest($3::uuid[]) AS id,
unnest($4::text[]) AS name,
unnest($5::text[]) AS workspace_folder,
unnest($6::text[]) AS config_path,
unnest($7::uuid[]) AS subagent_id
RETURNING workspace_agent_devcontainers.id, workspace_agent_devcontainers.workspace_agent_id, workspace_agent_devcontainers.created_at, workspace_agent_devcontainers.workspace_folder, workspace_agent_devcontainers.config_path, workspace_agent_devcontainers.name, workspace_agent_devcontainers.subagent_id
unnest($6::text[]) AS config_path
RETURNING workspace_agent_devcontainers.id, workspace_agent_devcontainers.workspace_agent_id, workspace_agent_devcontainers.created_at, workspace_agent_devcontainers.workspace_folder, workspace_agent_devcontainers.config_path, workspace_agent_devcontainers.name
`
type InsertWorkspaceAgentDevcontainersParams struct {
@@ -17397,7 +17395,6 @@ type InsertWorkspaceAgentDevcontainersParams struct {
Name []string `db:"name" json:"name"`
WorkspaceFolder []string `db:"workspace_folder" json:"workspace_folder"`
ConfigPath []string `db:"config_path" json:"config_path"`
SubagentID []uuid.UUID `db:"subagent_id" json:"subagent_id"`
}
func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error) {
@@ -17408,7 +17405,6 @@ func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg
pq.Array(arg.Name),
pq.Array(arg.WorkspaceFolder),
pq.Array(arg.ConfigPath),
pq.Array(arg.SubagentID),
)
if err != nil {
return nil, err
@@ -17424,7 +17420,6 @@ func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg
&i.WorkspaceFolder,
&i.ConfigPath,
&i.Name,
&i.SubagentID,
); err != nil {
return nil, err
}
@@ -1,14 +1,13 @@
-- name: InsertWorkspaceAgentDevcontainers :many
INSERT INTO
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path, subagent_id)
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path)
SELECT
@workspace_agent_id::uuid AS workspace_agent_id,
@created_at::timestamptz AS created_at,
unnest(@id::uuid[]) AS id,
unnest(@name::text[]) AS name,
unnest(@workspace_folder::text[]) AS workspace_folder,
unnest(@config_path::text[]) AS config_path,
unnest(@subagent_id::uuid[]) AS subagent_id
unnest(@config_path::text[]) AS config_path
RETURNING workspace_agent_devcontainers.*;
-- name: GetWorkspaceAgentDevcontainersByAgentID :many
@@ -2897,7 +2897,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
devcontainerNames = make([]string, 0, len(devcontainers))
devcontainerWorkspaceFolders = make([]string, 0, len(devcontainers))
devcontainerConfigPaths = make([]string, 0, len(devcontainers))
devcontainerSubagentIDs = make([]uuid.UUID, 0, len(devcontainers))
)
for _, dc := range devcontainers {
id := uuid.New()
@@ -2905,9 +2904,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
devcontainerNames = append(devcontainerNames, dc.Name)
devcontainerWorkspaceFolders = append(devcontainerWorkspaceFolders, dc.WorkspaceFolder)
devcontainerConfigPaths = append(devcontainerConfigPaths, dc.ConfigPath)
var subagentID uuid.UUID
copy(subagentID[:], dc.GetSubagentId())
devcontainerSubagentIDs = append(devcontainerSubagentIDs, subagentID)
// Add a log source and script for each devcontainer so we can
// track logs and timings for each devcontainer.
@@ -2936,7 +2932,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
Name: devcontainerNames,
WorkspaceFolder: devcontainerWorkspaceFolders,
ConfigPath: devcontainerConfigPaths,
SubagentID: devcontainerSubagentIDs,
})
if err != nil {
return xerrors.Errorf("insert agent devcontainer: %w", err)
@@ -3706,7 +3706,6 @@ func TestInsertWorkspaceResource(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{})
subagentID := uuid.New()
err := insert(db, job.ID, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
@@ -3715,7 +3714,6 @@ func TestInsertWorkspaceResource(t *testing.T) {
Devcontainers: []*sdkproto.Devcontainer{
{Name: "foo", WorkspaceFolder: "/workspace1"},
{Name: "bar", WorkspaceFolder: "/workspace2", ConfigPath: "/workspace2/.devcontainer/devcontainer.json"},
{Name: "baz", WorkspaceFolder: "/workspace3", SubagentId: subagentID[:]},
},
}},
})
@@ -3732,17 +3730,13 @@ func TestInsertWorkspaceResource(t *testing.T) {
return devcontainers[i].Name > devcontainers[j].Name
})
require.NoError(t, err)
require.Len(t, devcontainers, 3)
require.Len(t, devcontainers, 2)
require.Equal(t, "foo", devcontainers[0].Name)
require.Equal(t, "/workspace1", devcontainers[0].WorkspaceFolder)
require.Equal(t, "", devcontainers[0].ConfigPath)
require.Equal(t, uuid.Nil, devcontainers[0].SubagentID.UUID)
require.Equal(t, "baz", devcontainers[1].Name)
require.Equal(t, "/workspace3", devcontainers[1].WorkspaceFolder)
require.Equal(t, subagentID, devcontainers[1].SubagentID.UUID)
require.Equal(t, "bar", devcontainers[2].Name)
require.Equal(t, "/workspace2", devcontainers[2].WorkspaceFolder)
require.Equal(t, "/workspace2/.devcontainer/devcontainer.json", devcontainers[2].ConfigPath)
require.Equal(t, "bar", devcontainers[1].Name)
require.Equal(t, "/workspace2", devcontainers[1].WorkspaceFolder)
require.Equal(t, "/workspace2/.devcontainer/devcontainer.json", devcontainers[1].ConfigPath)
})
}
@@ -124,6 +124,77 @@ property in your `devcontainer.json`:
This maps container ports to the parent workspace, which can then be forwarded
using the main workspace agent.
## Docker Compose dev containers
Dev containers support Docker Compose for multi-container environments. When you
define a dev container with `dockerComposeFile` and `service` properties, the
devcontainer CLI orchestrates all services defined in your Compose file.
### Configuration
To use Docker Compose, your `devcontainer.json` must specify:
- `dockerComposeFile`: Path to your Docker Compose file(s)
- `service`: The container Coder connects to (becomes the dev container sub-agent)
```json
{
"name": "My Project",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace"
}
```
With a corresponding `docker-compose.yml`:
```yaml
services:
app:
image: mcr.microsoft.com/devcontainers/base:ubuntu
volumes:
- .:/workspace
command: sleep infinity
database:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
```
The `app` service becomes your dev container with full Coder integration (SSH,
web terminal, VS Code). The `database` service runs as a sidecar container.
### Container communication
Containers in a Compose setup communicate via Docker's internal DNS. From the
primary container, reach sidecar services by their service name:
```console
psql -h database -U postgres
```
### Accessing sidecar services
Since only the primary service container runs as a Coder sub-agent, you cannot
SSH or port-forward directly to sidecar containers. Instead:
1. **From the dev container**: Connect to sidecars using their service name
(e.g., `psql -h database`).
1. **From your local machine**: Access sidecar services through the primary
container. For example, run a proxy command in the dev container, then
port-forward that port.
### Limitations
- The `forwardPorts` property with `host:port` syntax (e.g., `"database:5432"`)
for forwarding ports from sidecar containers to your local machine is not yet
supported.
- Only the primary service container has Coder agent integration.
For more details on Docker Compose dev containers, see the
[Dev Container specification](https://containers.dev/implementors/spec/#docker-compose-based).
## Dev container features
You can use standard [dev container features](https://containers.dev/features)
+536 -593
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -244,11 +244,6 @@ message Devcontainer {
string workspace_folder = 1;
string config_path = 2;
string name = 3;
optional bytes id = 4;
optional bytes subagent_id = 5;
repeated App apps = 6;
repeated Script scripts = 7;
repeated Env envs = 8;
}
enum AppOpenIn {
-20
View File
@@ -306,11 +306,6 @@ export interface Devcontainer {
workspaceFolder: string;
configPath: string;
name: string;
id?: Uint8Array | undefined;
subagentId?: Uint8Array | undefined;
apps: App[];
scripts: Script[];
envs: Env[];
}
/** App represents a dev-accessible application on the workspace. */
@@ -1100,21 +1095,6 @@ export const Devcontainer = {
if (message.name !== "") {
writer.uint32(26).string(message.name);
}
if (message.id !== undefined) {
writer.uint32(34).bytes(message.id);
}
if (message.subagentId !== undefined) {
writer.uint32(42).bytes(message.subagentId);
}
for (const v of message.apps) {
App.encode(v!, writer.uint32(50).fork()).ldelim();
}
for (const v of message.scripts) {
Script.encode(v!, writer.uint32(58).fork()).ldelim();
}
for (const v of message.envs) {
Env.encode(v!, writer.uint32(66).fork()).ldelim();
}
return writer;
},
};