podspawnpodspawn

devcontainer.json Fallback

How podspawn converts devcontainer.json files to Podfile equivalents, including supported fields and conversion behavior.

If a project does not have a podfile.yaml but does have a devcontainer.json, podspawn will automatically convert it to an internal Podfile representation. This lets you use existing devcontainer configurations without rewriting them.

File search order

Podspawn looks for devcontainer files in this order:

  1. .devcontainer/devcontainer.json
  2. .devcontainer.json

The first match is used. This search only happens if no podfile.yaml was found.

Precedence

When both a Podfile and a devcontainer.json exist in the same project:

  1. podfile.yaml (or .podspawn/podfile.yaml) -- used, devcontainer ignored
  2. devcontainer.json -- converted to Podfile internally
  3. Neither -- server defaults apply

The Podfile always takes priority. There is no merging between the two formats.

Supported fields

devcontainer.json fieldPodfile equivalentNotes
imagebaseFalls back to ubuntu:24.04 if empty.
containerEnvenvMerged with remoteEnv.
remoteEnvenvMerged with containerEnv. remoteEnv wins on key conflicts.
forwardPortsports.exposeDirect mapping.
postCreateCommandon_createConverted to a shell string.
postStartCommandon_startConverted to a shell string.
features(recorded, not expanded)Features are parsed but not fully processed.
remoteUser(ignored)Parsed but has no effect. Podspawn always creates its own non-root user (UID 1000) with passwordless sudo, regardless of what remoteUser specifies.

JSONC support

devcontainer.json files commonly use JSONC (JSON with Comments). Podspawn strips both line comments (//) and block comments (/* */) before parsing.

{
  // Development container config
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "postCreateCommand": "npm install",
  /* Forward the dev server port */
  "forwardPorts": [3000]
}

Command conversion

The postCreateCommand and postStartCommand fields in devcontainer.json accept three formats. Podspawn converts all of them to a single shell string:

Used as-is:

{ "postCreateCommand": "npm install && npm run build" }

Joined with &&:

{ "postCreateCommand": ["npm install", "npm run build"] }

Becomes: npm install && npm run build

Values joined with && in sorted key order:

{
  "postCreateCommand": {
    "backend": "pip install -r requirements.txt",
    "frontend": "npm install"
  }
}

Becomes: pip install -r requirements.txt && npm install (keys sorted alphabetically).

The conversion is lossy. Features, Docker Compose configurations, Dockerfile-based builds, and other advanced devcontainer capabilities are not supported. For full control, write a podfile.yaml instead.

Converted hooks (postCreateCommand and postStartCommand) run as the non-root container user with sudo available, the same as native Podfile hooks. See Hooks & Lifecycle for details on execution context and error handling.

Example

{
  "image": "mcr.microsoft.com/devcontainers/python:3.12",
  "containerEnv": {
    "PYTHONDONTWRITEBYTECODE": "1"
  },
  "remoteEnv": {
    "EDITOR": "code"
  },
  "forwardPorts": [8000, 5432],
  "postCreateCommand": "pip install -r requirements.txt",
  "postStartCommand": "python manage.py migrate"
}
base: mcr.microsoft.com/devcontainers/python:3.12
shell: /bin/bash
env:
  PYTHONDONTWRITEBYTECODE: "1"
  EDITOR: code
ports:
  expose: [8000, 5432]
on_create: pip install -r requirements.txt
on_start: python manage.py migrate

Podspawn converts the devcontainer.json internally to this equivalent representation.

How is this guide?

On this page