@alexgiantwhale: https://x.com/alexgiantwhale/status/2079342482593309022

X AI KOLs Timeline Tools

Summary

A detailed technical article on how to build a Dev Container from scratch, including writing Dockerfile, devcontainer.json, configuring security options, and lifecycle commands to solidify the Go development environment and restrict container permissions.

https://t.co/5s6uyYd7oX
Original Article
View Cached Full Text

Cached at: 07/21/26, 04:46 PM

Building a Dev Container from Scratch: Putting Development Environment and Coding Agent Together in a Container

In my previous post, I used a minimal Docker experiment to verify one thing: the same process, with different mount points and runtime arguments, sees and modifies different things.
The principle isn’t complicated, but if I had to manually type a docker run command with a dozen parameters every day, I definitely wouldn’t stick with it.

A more practical approach is to put the development environment into the repository: new developers clone it and don’t need to install a bunch of tools first; CI, editors, and Coding Agents try to use the same environment as much as possible.
That’s the problem Dev Container aims to solve.

This post isn’t just about clicking “Reopen in Container” in VS Code. I’ll start with a real Go mini-project, write a .devcontainer/, start it with the official CLI, then enter the container to check UID, capabilities, seccomp, and workspace write permissions.

Let’s state the conclusion first: Dev Container is great for solidifying the development environment and can carry some permission configuration; but by default it prioritizes development experience, not an out-of-the-box security sandbox.

What Exactly is a Dev Container

Dev Container is an open specification, not a VS Code proprietary format.
It allows you to describe “how the development container should be created and used” using a devcontainer.json file in your repository; tools that support it translate these configurations into Docker parameters.

A typical directory looks like:

.devcontainer/
├── devcontainer.json
├── Dockerfile
└── verify.sh

The three files have different responsibilities:

FileResponsibility
DockerfileLong-term system, runtime, and OS dependencies in the image
devcontainer.jsonUser, mounts, ports, editor settings, lifecycle commands, and runtime parameters
verify.shCheck whether the final environment actually meets expectations

Think of it this way: the Dockerfile defines “what is installed in the development machine”, and devcontainer.json defines “how this development machine starts and how you connect to it.”

Step 1: Prepare the Development Image

The example project is the Go permission probe from the previous post. I’ll start with a very short Dockerfile:

FROM mcr.microsoft.com/devcontainers/go:1-1.22-bookworm

# Stable, project-wide OS dependencies go here.
# Do not put credentials into image layers.

I deliberately fixed the Go and Debian versions instead of using latest.
The development image will enter everyone’s daily environment. A floating tag builds today, but a few weeks later it might switch Go versions, system libraries, or even default configurations. When an upgrade is needed, you should actively change the version, rebuild, and run tests — not let the upgrade happen silently.

If you only need the official image, you can directly write image in devcontainer.json; only when you need apt packages, system libraries, or custom tools do you use a Dockerfile.

Step 2: Write devcontainer.json

This is the configuration I actually ran:

{
  "name": "Agent Sandbox Demo",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
  "remoteUser": "vscode",
  "containerUser": "vscode",
  "updateRemoteUserUID": true,
  "securityOpt": [
    "no-new-privileges",
    "seccomp=builtin"
  ],
  "runArgs": [
    "--cap-drop=ALL",
    "--pids-limit=256",
    "--memory=1g",
    "--cpus=2"
  ],
  "mounts": [
    "source=agent-sandbox-go-cache,target=/go/pkg/mod,type=volume"
  ],
  "postCreateCommand": "go test ./... && .devcontainer/verify.sh",
  "customizations": {
    "vscode": {
      "extensions": ["golang.go"],
      "settings": {
        "go.toolsManagement.autoUpdate": false
      }
    }
  }
}

Let’s go through each section.

build: Which image to start from

The path to the Dockerfile is relative to devcontainer.json. Because the Dockerfile is in .devcontainer/ and the build context needs to cover the project root, context is set to ...

The build context determines which files Docker build can see. Don’t blindly point the context to a much larger parent directory; use .dockerignore to exclude .git, build artifacts, and local temporary files.

remoteUser and containerUser: Don’t use root by default

These two names can be confusing:

  • containerUser controls which user the container runs as overall;
  • remoteUser controls which user the editor, terminal, tasks, and debugger use when connecting.

I set both to vscode (which is built into the image). updateRemoteUserUID: true attempts to align the container user’s UID/GID with the host user on Linux, reducing bind mount permission issues.

securityOpt and runArgs: Write boundaries into configuration

I set:

  • no-new-privileges: prevents processes from gaining new privileges via setuid, etc.;
  • seccomp=builtin: explicitly enables Docker’s built-in seccomp;
  • --cap-drop=ALL: drops all Linux capabilities;
  • PID, memory, and CPU limits: to prevent a runaway process from bringing down the entire development environment.

Note: I did not set --network none. The reason is pragmatic: daily development requires downloading Go modules, looking up documentation, accessing Git, and testing services.

So this is a “development-usable baseline”, not the highest isolation level for handling completely untrusted code. Network boundaries should be further tightened via proxies, domain allowlists, or isolated execution environments.

mounts: Only cache dependencies, not home directory

The Go module cache goes into a separate Docker volume so rebuilding the container doesn’t require re-downloading every time:

"mounts": [
  "source=agent-sandbox-go-cache,target=/go/pkg/mod,type=volume"
]

I did not mount ~/.ssh, ~/.aws, ~/.kube, or /var/run/docker.sock.
Dev Container supports mounting these directories, but “supported” does not mean “should”. Especially Docker Socket — once obtained, it usually means you can control the host Docker daemon, directly bypassing the container boundary.

Step 3: Understand Lifecycle Commands

The most common mistake with Dev Container is getting the lifecycle wrong.

ConfigurationWhen it runsBest used for
Dockerfile RUNDuring image buildInstalling stable system dependencies and runtimes
onCreateCommandFirst container creationEarliest project initialization
updateContentCommandOn creation and when code content updatesPreparation work related to repository content
postCreateCommandAfter container creation is completeInstalling project dependencies, running an initial check
postStartCommandEvery time the container startsStarting background services
postAttachCommandEvery time a tool connectsVery lightweight post-connect operations

In my example, I only use:

"postCreateCommand": "go test ./... && .devcontainer/verify.sh"

Because testing and environment verification should run once after the container is created, but not every time a terminal is opened.

Do not put long-term stable apt installations into postCreateCommand. That would make container creation dependent on the current network and package sources, and would give everyone a different base image. Anything that can be fixed in the Dockerfile should be put back into the Dockerfile.

Step 4: Start the Dev Container

Using VS Code, install the Dev Containers extension, open the project, and run:

Dev Containers: Reopen in Container

After modifying the Dockerfile or devcontainer.json, run:

Dev Containers: Rebuild Container

Simply reopening the window won’t automatically rebuild the image — this is a common “I changed the configuration, why didn’t it take effect?” scenario.

Using the official CLI

Dev Container doesn’t have to be started from the editor. The official CLI package is @devcontainers/cli:

npx -y @devcontainers/cli up --workspace-folder .

To execute commands inside the already started environment:

npx -y @devcontainers/cli exec \
  --workspace-folder . \
  sh -lc 'id; go version; pwd'

On July 16, 2026, using CLI version 0.87.0, I actually got:

uid=1000(vscode) gid=1000(vscode)
go version go1.22.12 linux/arm64
/workspaces/ai/labs/agent-sandbox-demo

These three outputs respectively confirm: the command is not run as root, Go comes from the Linux container, and the current directory is the workspace inside the container.

Step 5: Don’t Just Read the Configuration — Verify the Final Environment

In .devcontainer/verify.sh, I put a few assertions:

#!/bin/sh
set -eu

echo "user=$(id -un) uid=$(id -u) gid=$(id -g)"
echo "go=$(go version)"
echo "no_new_privs=$(awk '/^NoNewPrivs:/ { print $2 }' /proc/self/status)"
echo "effective_caps=$(awk '/^CapEff:/ { print $2 }' /proc/self/status)"
echo "seccomp=$(awk '/^Seccomp:/ { print $2 }' /proc/self/status)"

test "$(id -u)" -ne 0
test "$(awk '/^NoNewPrivs:/ { print $2 }' /proc/self/status)" = "1"
test "$(awk '/^CapEff:/ { print $2 }' /proc/self/status)" = "0000000000000000"
test "$(awk '/^Seccomp:/ { print $2 }' /proc/self/status)" = "2"

The actual output was:

user=vscode uid=1000 gid=1000
workspace=/workspaces/ai/labs/agent-sandbox-demo
go=go version go1.22.12 linux/arm64
no_new_privs=1
effective_caps=0000000000000000
seccomp=2
workspace_write=allowed

Here Seccomp: 2 means filtering mode is enabled, and CapEff all zeros means the current process has no effective capabilities.

The workspace being writable is not a vulnerability; it’s a functional requirement for this development environment: both developers and Coding Agents need to modify code. What should be restricted is “can only write to this workspace and specific cache/output directories,” not the illusion that a completely read-only environment can still accomplish programming tasks.

A Real Pitfall: Configuration Merges

On my first startup, even though I had written --cap-drop=ALL and no-new-privileges, the final Docker command ended up with:

--cap-add SYS_PTRACE --security-opt seccomp=unconfined

Entering the container to inspect:

NoNewPrivs: 1
Seccomp: 0

Seccomp: 0 means filtering was not enabled.

The reason was not that the Dev Container CLI randomly changed the configuration; it’s that the Dev Container specification allows writing metadata into image labels. The official Go development image, to support debuggers, declares SYS_PTRACE and seccomp=unconfined in its image metadata; the tool merges the image metadata, features, and the repository’s devcontainer.json.

I finally added the following explicitly in my project configuration:

"securityOpt": [
  "no-new-privileges",
  "seccomp=builtin"
]

After rebuilding, the final Docker command had both seccomp=unconfined and the later seccomp=builtin; Docker used the latter, and Seccomp inside the container became 2.

SYS_PTRACE also has a finer trade-off: the effective capabilities of the non-root development user are 0, but the capability bounding set still retains the SYS_PTRACE bit. This is convenient for debuggers, but also shows this is not an “absolute minimum privilege” image.

If your environment doesn’t need a debugger, or you want to run truly untrusted code, you should switch to a base image you control that doesn’t have such Dev Container metadata, and check the final generated Docker parameters and /proc/self/status — not just review the superficial JSON.

How Coding Agents Use This Environment

The key isn’t “the repository has a .devcontainer”, but where the Agent’s commands actually execute.

Three common scenarios:

  • Agent runs inside the Dev Container: the Shell, Go, Git it invokes are naturally inside the container;
  • Agent runs on the host but uses devcontainer exec to execute project commands: critical operations go into the container, but the host Agent itself still has host privileges;
  • Agent only reads .devcontainer but actually runs commands on the host: this provides no isolation.

Therefore, at least have the Agent or automation script first execute:

id
uname -a
cat /proc/1/cgroup
pwd
go version

Confirm the user, kernel environment, cgroup, path, and toolchain match expectations before starting to modify and test.

For an Agent on the host, I prefer to wrap project commands uniformly as:

npx -y @devcontainers/cli exec \
  --workspace-folder . \
  go test ./...

This at least avoids the dual-environment problem where “the editor is in the container, but the Agent silently calls the host’s Go.”

What Dev Container Solves and What It Doesn’t

It excels at:

  • Quickly giving new developers or new machines a consistent toolchain;
  • Versioning runtimes, dependencies, and initialization steps;
  • Letting editors, CLI, CI, and Agents reuse the same environment definition as much as possible;
  • Putting users, mounts, ports, and some Docker runtime parameters into code review.

It doesn’t automatically solve:

  • The default workspace is usually writable, and the network is usually available;
  • The initializeCommand in the repository may run directly on the host;
  • Image metadata and Features may add privileges or execute installation scripts;
  • Once Docker Socket, Home directories, and credentials are mounted, the container can access them;
  • Ordinary Docker containers still share the host kernel and are not equivalent to VM security boundaries.

Especially when receiving a .devcontainer from an unfamiliar repository, don’t just click “OK” because you see the word “container”. First review the Dockerfile, Compose, Features, mounts, initializeCommand, privileged, capAdd, securityOpt, and runArgs.

My Final Recommendation

If the goal is to unify the team’s development environment, Dev Container is well worth using.
If the goal is to make a Coding Agent pollute the host less, it’s also a very practical entry point: toolchain in a container, project permissions explicitly described, and the environment can be rebuilt when done.

But if the goal is to securely run code of unknown origin that might actively attack the environment, Dev Container alone is not enough. At minimum, consider Rootless Docker, strict network egress, short-lived secrets, and isolated working copies; for higher risk, use gVisor, Firecracker, or ephemeral VMs.

Summary in one sentence:

The Dockerfile solidifies “what is installed”, devcontainer.json solidifies “how to develop”, and the verification script confirms “what permissions were finally granted.”

Only when all three layers are reviewable, reproducible, and verifiable does Dev Container become more than just “it also runs on another machine” – it becomes a development environment truly suitable for both humans and Coding Agents to share.

Full example:
https://github.com/your-username/agent-sandbox-demo

Is your current development environment installed directly on the host, or is it already containerized?

Similar Articles

@1YES_yes1: Apple finally takes on Docker. Now that AI Coding has become popular, more and more people are running containers locally on Mac, deploying various Agent backends. But there's one thing that many have tolerated for a long time. Docker Desktop is just too heavy. Slow startup, high resource usage, fans...

X AI KOLs Timeline

Apple has open-sourced its official container tool Container on GitHub, rewritten in Swift and optimized for Apple Silicon. It is lighter and faster than Docker Desktop, supports direct installation and is compatible with Docker commands.

@thinkszyg: https://x.com/thinkszyg/status/2066837941477920993

X AI KOLs Timeline

A practical guide for developers (especially AI coding tool users) on how to safely and efficiently use Claude Code, Codex, and other tools for multi-agent parallel development, focusing on best practices such as task decomposition, file isolation (worktree), boundary control, sequential merging, etc., to avoid file conflicts and chaos.

@linghucong: https://x.com/linghucong/status/2068860590966321370

X AI KOLs Timeline

This article shares practical experience of using Codex /goal mode for long-term unattended programming, including how to write effective prompts, using persistent project memory to prevent deviation, and key settings and precautions.