Summary #
For programmatically controlling isolated environments with TypeScript, @dagger.io/dagger is the standard and most robust choice. It provides a strongly-typed, low-level SDK that allows an agent to construct, manipulate, and execute containers as if they were local objects.
Unlike higher-level abstractions that might give you a "pre-baked" dev environment, using the SDK directly gives your agent "God mode" over the container: it can swap base images, mount specific directories, inject secrets, and execute arbitrary commands, all while keeping the host system completely safe[1].
Philosophy & Mental Model #
The most critical concept for an Agent to understand is Immutability.
- Chains, Not Shells: You don't "open a terminal" and type commands. You take a
Containerobject and apply a transformation (like.withExec()) to get a newContainerobject. - Lazy Execution: Defining the pipeline does nothing. The container is only built and commands run when you call a terminal method like
.stdout(),.sync(), or.export(). - State Passing: To "keep" the state of a session (e.g., after
npm install), the Agent must hold onto the resulting container object and use it as the base for the next command.
The Mental Model:
Instead of: SSH -> Run Command -> Wait -> Run Next Command
Think: Base State + Command A = State A; then State A + Command B = State B.
Setup #
The agent needs the NPM package, but the host machine must have the Dagger engine installed.
1. Install Dagger Engine (Host) #
1# On the machine running the agent:
2curl -L https://dl.dagger.io/dagger/install.sh | sh
2. Install SDK #
1npm install @dagger.io/dagger
Core Usage Patterns #
Pattern 1: The "Stateful" Session #
This simulates a persistent workspace where the agent runs multiple commands in sequence, preserving filesystem changes.
1import { dag, Container, Directory } from "@dagger.io/dagger";
2
3async function runSession() {
4 // 1. Initialize a base container (e.g., Node.js)
5 let session: Container = dag
6 .container()
7 .from("node:22-alpine")
8 .withWorkdir("/app");
9
10 // 2. "Write" code (simulated by mounting or creating a file)
11 // In a real agent, you might mount a directory from the host
12 session = session.withNewFile("index.js", "console.log('Hello from Dagger');");
13
14 // 3. Run a command (State A -> State B)
15 // We capture the NEW container state
16 session = session.withExec(["node", "index.js"]);
17
18 // 4. Extract output (triggers execution)
19 const output = await session.stdout();
20 console.log("Agent received:", output); // "Hello from Dagger"
21
22 // 5. Continue the session (State B -> State C)
23 session = session.withExec(["touch", "test-passed.txt"]);
24
25 // Verify file exists
26 const files = await session.directory(".").entries();
27 console.log("Files:", files); // ['index.js', 'test-passed.txt']
28}
Pattern 2: Dynamic Tool Construction #
The agent can build its own tools on the fly. If it needs ffmpeg but the container doesn't have it, it installs it.
1async function executeWithTool(toolName: string, args: string[]) {
2 // Agent decides it needs a specific environment
3 let ctr = dag.container().from("ubuntu:latest");
4
5 // Dynamically install dependencies
6 ctr = ctr
7 .withExec(["apt-get", "update"])
8 .withExec(["apt-get", "install", "-y", toolName]);
9
10 // Run the requested tool
11 ctr = ctr.withExec([toolName, ...args]);
12
13 return await ctr.stdout();
14}
Pattern 3: Safe File Extraction #
When the agent writes code or artifacts inside the container, retrieve them safely without exposing the host fs directly.
1async function retrieveArtifact(container: Container, path: string) {
2 // Get a reference to the file inside the container
3 const file = container.file(path);
4
5 // Read content directly into memory (good for text)
6 const content = await file.contents();
7 return content;
8}
Anti-Patterns & Pitfalls #
❌ Don't: Ignoring the Return Value #
Dagger objects are immutable. Modifying them returns a new object.
1// BAD
2const ctr = dag.container().from("alpine");
3ctr.withExec(["echo", "hello"]); // THIS DOES NOTHING to 'ctr'
4await ctr.stdout(); // Error or empty output, because 'ctr' is still just the base image
✅ Instead: Chain or Reassign #
1// GOOD
2let ctr = dag.container().from("alpine");
3ctr = ctr.withExec(["echo", "hello"]); // Update the reference
4await ctr.stdout();
❌ Don't: treating withExec as "Run Now" #
withExec configures the plan. It does not execute until you await a result.
1// BAD
2ctr.withExec(["long-process"]); // Returns instantly
3console.log("Done?"); // Prints immediately, process hasn't run
✅ Instead: Await a Terminal Operation #
1// GOOD
2// .sync() forces execution without returning output data
3await ctr.withExec(["long-process"]).sync();
4console.log("Done!");
Caveats #
- Cold Starts: Pulling the
from(...)image takes time on the first run. Cache persists locally, so subsequent runs are fast. - Engine Requirement: This is not a pure Node.js library; it communicates with the Dagger Engine (a distinct binary/service). Your deployment environment must support this.
- Error Handling: If a command in
.withExec()fails (non-zero exit code), the Promise rejects. You must wrap chains intry/catchto handle "runtime" errors gracefully.
References #
[1] Dagger TypeScript SDK Reference - Documentation [2] Dagger API Concepts - Mental Model [3] Dagger NPM Package - npm