
Devcontainer
- 297 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
devcontainer is a Claude Code skill that configures and uses devcontainers so Claude-assisted development runs in a reproducible, dependency-complete environment for developers who need consistent setups across machines
About
devcontainer is an agent skill from jwynia/agent-skills for configuring VS Code/Cursor devcontainers so AI-assisted development runs inside a reproducible Docker-based environment. It helps developers define devcontainer.json, Dockerfile references, feature packs, and post-create commands so every contributor gets the same language runtimes, CLIs, and extensions. Teams reach for it when onboarding is slow, local dependency drift breaks agent workflows, or remote-container development must mirror CI. The skill focuses on environment parity for coding agents, not production deployment pipelines.
- Standardizes reproducible dev environments
- Reduces onboarding and dependency drift
- Works with containerized project setups
- Improves cross-machine parity for agents
- Supports consistent tooling in Codespaces-like flows
Devcontainer by the numbers
- 297 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #334 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill devcontainerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 297 |
|---|---|
| repo stars | ★ 133 |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do you set up reproducible devcontainers for AI coding?
Configure and use devcontainers so Claude-assisted development runs in a reproducible, dependency-complete environment across machines and contributors.
Who is it for?
Developers standardizing local and remote dev environments for Claude Code or Cursor across a team repository.
Skip if: Operators who only need production Kubernetes manifests or one-off local installs without containerized dev parity.
When should I use this skill?
User asks to configure devcontainers, devcontainer.json, reproducible dev environments, or containerized Claude development setups.
What you get
devcontainer.json configuration, Dockerfile or image reference, feature pack selections, and post-create setup steps.
- devcontainer.json
- Container build configuration
- Post-create setup script guidance
Files
Devcontainer Diagnostic
Diagnose devcontainer and Docker development environment problems. Help create reproducible, fast-starting development environments that work consistently across VS Code, GitHub Codespaces, and team members.
When to Use This Skill
Use this skill when:
- Setting up a new devcontainer
- Container startup is too slow
- Configuration errors or conflicts
- Different behavior in VS Code vs Codespaces
- Multi-service development environment needed
Do NOT use this skill when:
- Writing application code
- Deploying to production
- Configuring CI/CD pipelines
Core Principle
Development containers should provide instant productivity. Every configuration choice affects startup time, reproducibility, and team onboarding. Make these trade-offs explicit.
Diagnostic States
DV0: No Devcontainer Strategy
Symptoms: Manual setup, "check the README", works on one machine fails on others
Interventions:
- Start with pre-built devcontainer base image
- Use
assets/devcontainer-simple.mdtemplate
DV1: Slow Container Startup
Symptoms: 5+ minute startup, heavy postCreateCommand, avoiding rebuilds
Key Questions:
- How long does startup actually take?
- What's in postCreateCommand?
- Are you using prebuilds?
Interventions:
- Move npm install/pip install to Dockerfile (cached)
- Use mcr.microsoft.com/devcontainers/* base images
- Configure prebuilds for team repos
- Run
scripts/analyze-devcontainer.ts
DV2: Configuration Problems
Symptoms: JSON errors, VS Code won't connect, features conflicting
Checklist:
- [ ] devcontainer.json passes JSON validation
- [ ] Using only ONE of: image, build.dockerfile, dockerComposeFile
- [ ] Features are compatible and ordered correctly
- [ ] Extensions use correct publisher.extension-name format
DV3: Environment Parity Issues
Symptoms: Works in VS Code, fails in Codespaces (or vice versa)
Common Issues:
| Issue | Local VS Code | Codespaces |
|---|---|---|
| Docker socket | Usually available | Docker-in-Docker needed |
| Secrets | .env files work | Use Codespaces secrets |
| File watching | Native | May need polling |
DV4: Multi-Service Complexity
Symptoms: Need database/cache/queue, services can't communicate
Interventions:
- Use Docker Compose integration
- Named volumes for persistence
- Health checks for service readiness
- Use
assets/devcontainer-compose.mdtemplate
DV5: Dockerfile Issues
Symptoms: Build failures, huge images, no caching
Best Practices:
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# Dependencies first (cached)
RUN apt-get update && apt-get install -y \
build-essential && rm -rf /var/lib/apt/lists/*
# Copy deps then install (cached if deps unchanged)
COPY package*.json ./
RUN npm install
# Code last (changes frequently)
COPY . .DV6: Devcontainer Validated
Indicators:
- Startup under 2 minutes
- Works in VS Code and Codespaces
- New developers productive in 30 minutes
Available Scripts
| Script | Purpose | Usage |
|---|---|---|
analyze-devcontainer.ts | Find issues and optimizations | deno run --allow-read scripts/analyze-devcontainer.ts |
validate-dockerfile.ts | Check Dockerfile best practices | deno run --allow-read scripts/validate-dockerfile.ts |
scan-image.ts | Vulnerability scanning (wraps Trivy) | deno run --allow-run scripts/scan-image.ts [image] |
Anti-Patterns
The Kitchen Sink
Installing every tool "just in case" - 10+ minute startups. Fix: Start minimal. Add only when needed.
The postCreateCommand Overload
Everything in postCreateCommand - runs every time. Fix: Move stable operations to Dockerfile.
The Snowflake Container
Manual changes inside running containers. Fix: ALL changes go in config files.
Templates
assets/devcontainer-simple.md- Basic single-container setupassets/devcontainer-dockerfile.md- Custom Dockerfile approachassets/devcontainer-compose.md- Multi-service setup
Related Skills
- system-design - Multi-service architecture informs Compose config
- pwa-development - Consistent environment for PWA toolchain
Docker Compose-Based Devcontainer Template
Use this template when:
- Your app needs multiple services (database, cache, queue)
- You want to develop against a production-like environment
- Services need to communicate with each other
- You need persistent data volumes
Directory Structure
.devcontainer/
├── devcontainer.json
├── docker-compose.yml
├── Dockerfile # Optional: for app service
└── .env # Environment variablesBasic Multi-Service Template
devcontainer.json
{
"name": "Full Stack Dev",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker"
]
}
},
"forwardPorts": [3000, 5432],
"portsAttributes": {
"3000": { "label": "App" },
"5432": { "label": "PostgreSQL" }
},
"postCreateCommand": "npm install",
"remoteUser": "vscode"
}docker-compose.yml
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/dev
networks:
- dev-network
db:
image: postgres:15
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dev
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- dev-network
volumes:
postgres-data:
networks:
dev-network:Dockerfile (for app service)
FROM mcr.microsoft.com/devcontainers/javascript-node:20
# Install any additional system packages
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
USER node
WORKDIR /workspaceCommon Service Combinations
Node.js + PostgreSQL + Redis
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/dev
- REDIS_URL=redis://redis:6379
networks:
- dev-network
db:
image: postgres:15
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dev
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- dev-network
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- dev-network
volumes:
postgres-data:
redis-data:
networks:
dev-network:Python + MySQL + RabbitMQ
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
depends_on:
db:
condition: service_healthy
rabbitmq:
condition: service_healthy
environment:
- DATABASE_URL=mysql://root:root@db:3306/dev
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
networks:
- dev-network
db:
image: mysql:8
restart: unless-stopped
volumes:
- mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dev
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 5
networks:
- dev-network
rabbitmq:
image: rabbitmq:3-management
restart: unless-stopped
volumes:
- rabbitmq-data:/var/lib/rabbitmq
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 5s
timeout: 5s
retries: 5
networks:
- dev-network
volumes:
mysql-data:
rabbitmq-data:
networks:
dev-network:Microservices Development
version: '3.8'
services:
# Main development service (VS Code attaches here)
api:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
depends_on:
- db
- auth-service
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/api
- AUTH_SERVICE_URL=http://auth-service:3001
ports:
- "3000:3000"
networks:
- dev-network
# Additional service that's part of the system
auth-service:
build:
context: ../auth-service
dockerfile: Dockerfile
volumes:
- ../auth-service:/app:cached
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/auth
ports:
- "3001:3001"
networks:
- dev-network
db:
image: postgres:15
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
networks:
- dev-network
volumes:
postgres-data:
networks:
dev-network:Configuration Patterns
Environment Variables from File
.devcontainer/.env:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
DATABASE_URL=postgresql://postgres:postgres@db:5432/devdocker-compose.yml:
services:
app:
env_file:
- .envVolume Mounting Strategies
services:
app:
volumes:
# Source code: cached for performance
- ..:/workspace:cached
# Node modules: named volume for speed
- node_modules:/workspace/node_modules
# Persistent user config
- vscode-extensions:/home/vscode/.vscode-server/extensions
volumes:
node_modules:
vscode-extensions:Health Checks for Dependencies
services:
app:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5Networking
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend
nginx:
networks:
- frontend
networks:
frontend:
backend:Service discovery via DNS: http://db:5432, http://redis:6379
Port Forwarding
devcontainer.json:
{
"forwardPorts": [3000, 5432, 6379],
"portsAttributes": {
"3000": {
"label": "Application",
"onAutoForward": "openBrowser"
},
"5432": {
"label": "PostgreSQL",
"onAutoForward": "silent"
},
"6379": {
"label": "Redis",
"onAutoForward": "silent"
}
}
}Running Multiple Services
If you need VS Code attached to multiple services, create multiple devcontainer configs:
.devcontainer/api/devcontainer.json:
{
"name": "API Development",
"dockerComposeFile": "../docker-compose.yml",
"service": "api",
"workspaceFolder": "/workspace"
}.devcontainer/auth/devcontainer.json:
{
"name": "Auth Service Development",
"dockerComposeFile": "../docker-compose.yml",
"service": "auth-service",
"workspaceFolder": "/app"
}Common Issues
Services Can't Connect
# Ensure all services on same network
services:
app:
networks:
- dev-network
db:
networks:
- dev-network
networks:
dev-network:Data Not Persisting
# Use named volumes, not bind mounts for databases
volumes:
postgres-data: # Named volume
services:
db:
volumes:
- postgres-data:/var/lib/postgresql/data # Persists across rebuildsSlow File System
# Use cached consistency for source code
volumes:
- ..:/workspace:cached # Improves macOS performance
# Use delegated for write-heavy directories
- ../logs:/workspace/logs:delegatedContainer Startup Order
services:
app:
depends_on:
db:
condition: service_healthy # Wait for health check
restart: unless-stopped # Restart if DB not readyGitHub Codespaces Considerations
- Services run inside the Codespace VM (not separate VMs)
- Port forwarding works automatically
- Named volumes persist within the Codespace
- Some volume mounts may behave differently
Test in both local VS Code and Codespaces to ensure compatibility.
Dockerfile-Based Devcontainer Template
Use this template when:
- You need custom system packages
- You want to optimize startup time by caching installations
- You need specific tool versions not available as features
- You're building a production-like development environment
- You want fine-grained control over the image
Directory Structure
.devcontainer/
├── devcontainer.json
├── Dockerfile
└── .dockerignoreBasic Template
devcontainer.json
{
"name": "Custom Dev Environment",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode"
]
}
},
"forwardPorts": [3000],
"postCreateCommand": "npm install",
"remoteUser": "vscode"
}Dockerfile
# Use devcontainer base for proper user setup
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# Install system packages (cached layer)
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Switch to vscode user for subsequent operations
USER vscode
# Set working directory
WORKDIR /workspace.dockerignore
node_modules
.git
*.log
.env
dist
build
coverage
.cacheLanguage-Specific Templates
Node.js with Optimized Caching
FROM mcr.microsoft.com/devcontainers/javascript-node:20
# Install global packages (rarely changes)
RUN npm install -g typescript ts-node
# Copy package files first for caching
COPY package*.json ./
# Install dependencies (cached unless package.json changes)
RUN npm install
# Copy rest of code (changes frequently)
COPY . .
USER node
WORKDIR /workspaceWith this setup, postCreateCommand can be empty or just run migrations.
Python with Poetry
FROM mcr.microsoft.com/devcontainers/python:3.11
# Install Poetry
RUN curl -sSL https://install.python-poetry.org | python3 -
# Add Poetry to PATH
ENV PATH="/home/vscode/.local/bin:$PATH"
# Copy dependency files
COPY pyproject.toml poetry.lock* ./
# Install dependencies (without creating virtualenv inside container)
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
USER vscode
WORKDIR /workspaceGo with Tools
FROM mcr.microsoft.com/devcontainers/go:1.21
# Install additional Go tools
RUN go install github.com/cosmtrek/air@latest \
&& go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Copy go.mod and go.sum for dependency caching
COPY go.mod go.sum ./
RUN go mod download
USER vscode
WORKDIR /workspaceMulti-Stage for Complex Setups
# Build stage - install heavy dependencies
FROM mcr.microsoft.com/devcontainers/base:ubuntu AS builder
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
&& rm -rf /var/lib/apt/lists/*
# Final development stage
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# Copy only built artifacts from builder
COPY --from=builder /usr/local/bin/custom-tool /usr/local/bin/
# Development-specific tools
RUN apt-get update && apt-get install -y \
git \
vim \
&& rm -rf /var/lib/apt/lists/*
USER vscode
WORKDIR /workspaceOptimization Patterns
Layer Ordering for Cache Efficiency
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# 1. System packages (rarely change)
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 2. Global tools (occasionally change)
RUN npm install -g typescript
# 3. Dependency files (change with project updates)
COPY package*.json ./
RUN npm install
# 4. Source code (changes frequently)
COPY . .Cleanup in Same Layer
# BAD: Cleanup in separate layer doesn't reduce size
RUN apt-get update && apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*
# GOOD: Cleanup in same layer
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*Using ARG for Flexible Versions
FROM mcr.microsoft.com/devcontainers/base:ubuntu
ARG NODE_VERSION=20
ARG PYTHON_VERSION=3.11
# Use in installation
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejsOverride at build time:
{
"build": {
"dockerfile": "Dockerfile",
"args": {
"NODE_VERSION": "18"
}
}
}Security Best Practices
Run as Non-Root
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# Do root operations
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Switch to non-root user
USER vscode
WORKDIR /home/vscode
# User-level installations
RUN npm install -g typescriptPin Versions
# Good: Pinned versions
FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04
# Avoid: Unpinned versions
FROM mcr.microsoft.com/devcontainers/base:ubuntuAvoid Secrets in Dockerfile
# BAD: Secret in image layer
ENV API_KEY=secret123
# GOOD: Use runtime environment
# Set in devcontainer.json remoteEnv or Codespaces secretsdevcontainer.json Integration
{
"build": {
"dockerfile": "Dockerfile",
"context": "..",
"args": {
"NODE_VERSION": "20"
}
},
// Features still work with Dockerfile
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
// Only run what MUST happen after container creation
"postCreateCommand": "npm run setup:dev",
"remoteUser": "vscode"
}Debugging Build Issues
View Build Output
# Full build output
docker build --progress=plain -t test .
# No cache (rebuild from scratch)
docker build --no-cache -t test .Inspect Layers
# See layer sizes
docker history <image>
# Detailed layer analysis
docker inspect <image>Test Without VS Code
# Build and run interactively
docker build -t dev-test .
docker run -it dev-test /bin/bashWhen to Graduate to Docker Compose
Move to Docker Compose when:
- You need additional services (database, cache, queue)
- Services need to communicate via network
- You want to match production multi-container setup
See devcontainer-compose.md template.
Simple Image-Based Devcontainer Template
Use this template when:
- You want the fastest setup
- You're using a standard language/framework
- You don't need custom system packages
- The base image already has what you need
Basic Template
Create .devcontainer/devcontainer.json:
{
"name": "Project Name",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
// Features add common tools without custom Dockerfile
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
// VS Code customization
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
],
"settings": {
"editor.formatOnSave": true
}
}
},
// Port forwarding for dev servers
"forwardPorts": [3000],
// Commands
"postCreateCommand": "echo 'Ready to code!'",
// Run as non-root user
"remoteUser": "vscode"
}Language-Specific Templates
Node.js
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"forwardPorts": [3000],
"postCreateCommand": "npm install",
"remoteUser": "node"
}Python
{
"name": "Python",
"image": "mcr.microsoft.com/devcontainers/python:3.11",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python"
}
}
},
"postCreateCommand": "pip install -r requirements.txt",
"remoteUser": "vscode"
}Go
{
"name": "Go",
"image": "mcr.microsoft.com/devcontainers/go:1.21",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"golang.go"
]
}
},
"postCreateCommand": "go mod download",
"remoteUser": "vscode"
}Rust
{
"name": "Rust",
"image": "mcr.microsoft.com/devcontainers/rust:1",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer"
]
}
},
"remoteUser": "vscode"
}TypeScript (Deno)
{
"name": "Deno",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/anthropics/devcontainer-features/deno:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"denoland.vscode-deno"
],
"settings": {
"deno.enable": true
}
}
},
"remoteUser": "vscode"
}Common Patterns
Environment Variables
{
// Set during container creation (available in Dockerfile)
"containerEnv": {
"NODE_ENV": "development"
},
// Set at runtime (available in shell)
"remoteEnv": {
"API_URL": "${localEnv:API_URL}",
"DEBUG": "true"
}
}Port Forwarding
{
"forwardPorts": [3000, 5432],
"portsAttributes": {
"3000": {
"label": "Frontend",
"onAutoForward": "notify"
},
"5432": {
"label": "Database",
"onAutoForward": "silent"
}
}
}Mounting Local Files
{
"mounts": [
"source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,readonly"
]
}GitHub Codespaces Secrets
Secrets are available as environment variables. Reference them:
{
"remoteEnv": {
"DATABASE_URL": "${localEnv:DATABASE_URL}"
}
}Set secrets in GitHub: Settings → Codespaces → Secrets
When to Graduate to Dockerfile
Move to a Dockerfile-based setup when:
- You need system packages not available as features
- postCreateCommand is slow and could be cached
- You need specific versions of tools
- You want smaller image size
- You need multi-stage builds
See devcontainer-dockerfile.md template.
Devcontainer Features Catalog
Features are pre-packaged tools and runtimes that can be added to any devcontainer. They install on top of any base image, making configuration modular and reusable.
Official Features Repository: https://github.com/devcontainers/features
How to Use Features
{
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/git:1": {}
}
}Core Development Tools
Git
Essential for most development workflows.
"ghcr.io/devcontainers/features/git:1": {}Options:
version: Git version (default: "latest")ppa: Use PPA for latest version (default: true)
GitHub CLI
For GitHub operations from the command line.
"ghcr.io/devcontainers/features/github-cli:1": {}Options:
version: CLI version (default: "latest")
Docker-in-Docker
Run Docker commands inside the devcontainer (for container development).
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": true
}Options:
version: Docker versionmoby: Use Moby (open source Docker, default: true)dockerDashComposeVersion: Compose version
Note: May not work in all Codespaces configurations. Test thoroughly.
Docker-outside-of-Docker
Use host's Docker daemon (lighter than docker-in-docker).
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}Languages & Runtimes
Node.js
"ghcr.io/devcontainers/features/node:1": {
"version": "20",
"nodeGypDependencies": true
}Options:
version: Node version ("lts", "20", "18", etc.)nodeGypDependencies: Install build tools for native modulesnvmVersion: NVM version to install
Python
"ghcr.io/devcontainers/features/python:1": {
"version": "3.11"
}Options:
version: Python versioninstallTools: Install pip, pipx, etc. (default: true)optimize: Optimize Python installation (default: false)
Go
"ghcr.io/devcontainers/features/go:1": {
"version": "1.21"
}Options:
version: Go version
Rust
"ghcr.io/devcontainers/features/rust:1": {
"version": "latest",
"profile": "default"
}Options:
version: Rust versionprofile: Rustup profile (minimal, default, complete)
Java
"ghcr.io/devcontainers/features/java:1": {
"version": "17",
"installMaven": true,
"installGradle": true
}Options:
version: Java versioninstallMaven: Install MaveninstallGradle: Install GradlejdkDistro: JDK distribution (ms, default)
.NET
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "8.0"
}Options:
version: .NET versionaspnetcore: Install ASP.NET Core runtime
PHP
"ghcr.io/devcontainers/features/php:1": {
"version": "8.2",
"installComposer": true
}Options:
version: PHP versioninstallComposer: Install Composer
Ruby
"ghcr.io/devcontainers/features/ruby:1": {
"version": "3.2"
}Options:
version: Ruby version
Shell & Terminal
Oh My Zsh
"ghcr.io/devcontainers/features/omz:1": {
"plugins": "git docker kubectl"
}Options:
plugins: Space-separated plugin listtheme: Theme name (default: "robbyrussell")
Fish Shell
"ghcr.io/devcontainers/features/fish:1": {}Starship Prompt
"ghcr.io/devcontainers/features/starship:1": {}Cloud & Infrastructure
AWS CLI
"ghcr.io/devcontainers/features/aws-cli:1": {}Azure CLI
"ghcr.io/devcontainers/features/azure-cli:1": {}Google Cloud CLI
"ghcr.io/devcontainers/features/gcloud:1": {}Terraform
"ghcr.io/devcontainers/features/terraform:1": {
"version": "latest",
"tflint": "latest"
}Options:
version: Terraform versiontflint: TFLint versionterragrunt: Terragrunt version
Kubectl & Helm
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
"version": "latest",
"helm": "latest",
"minikube": "latest"
}Databases & Data Tools
PostgreSQL Client
"ghcr.io/devcontainers/features/postgresql-client:1": {}MySQL Client
"ghcr.io/devcontainers/features/mysql-client:1": {}Redis Client (redis-cli)
"ghcr.io/devcontainers/features/redis-cli:1": {}MongoDB Shell
"ghcr.io/devcontainers/features/mongodb-community:1": {}Utilities
jq (JSON processor)
"ghcr.io/devcontainers/features/jq:1": {}yq (YAML processor)
"ghcr.io/devcontainers/features/yq:1": {}HTTPie (HTTP client)
"ghcr.io/devcontainers/features/httpie:1": {}Deno
"ghcr.io/anthropics/devcontainer-features/deno:1": {}Community Features
Beyond official features, many community features exist:
Bun
"ghcr.io/shyim/devcontainers-features/bun:1": {}pnpm
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}Yarn
"ghcr.io/devcontainers/features/yarn:1": {}Biome (linter/formatter)
"ghcr.io/biomejs/devcontainer-features/biome:1": {}Feature Configuration Patterns
Version Pinning
{
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20.10.0"
}
}
}Multiple Language Versions
{
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.11"
}
}
}Override Install Order
{
"features": {
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers/features/python:1": {}
},
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/python",
"ghcr.io/devcontainers/features/node"
]
}Common Feature Combinations
Full Stack JavaScript
{
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "20" },
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}
}Python Data Science
{
"features": {
"ghcr.io/devcontainers/features/python:1": { "version": "3.11" },
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/postgresql-client:1": {}
}
}DevOps/Platform Engineering
{
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {},
"ghcr.io/devcontainers/features/terraform:1": {},
"ghcr.io/devcontainers/features/aws-cli:1": {}
}
}Go Microservices
{
"features": {
"ghcr.io/devcontainers/features/go:1": { "version": "1.21" },
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/postgresql-client:1": {}
}
}Troubleshooting Features
Feature Not Installing
1. Check feature name spelling (case-sensitive) 2. Check version exists 3. Review devcontainer creation logs 4. Try rebuilding without cache
Feature Conflicts
Some features may conflict:
- Multiple versions of same language
- Docker-in-Docker vs Docker-outside-of-Docker
- Different shell configurations
Use overrideFeatureInstallOrder to control order.
Slow Feature Installation
Features install sequentially. For faster startup: 1. Move stable tools to Dockerfile 2. Use prebuilds for team repositories 3. Only include actually needed features
Finding Feature Options
1. Check official docs: https://containers.dev/features 2. Check feature's GitHub repo README 3. Look at devcontainer-feature.json in feature source
#!/usr/bin/env -S deno run --allow-read
/**
* Devcontainer Analyzer
*
* Analyzes devcontainer.json for common issues and optimization opportunities.
*
* Usage:
* deno run --allow-read scripts/analyze-devcontainer.ts [path-to-devcontainer.json]
*
* If no path provided, searches for .devcontainer/devcontainer.json in current directory.
*/
interface DevcontainerConfig {
name?: string;
image?: string;
build?: {
dockerfile?: string;
context?: string;
args?: Record<string, string>;
};
dockerComposeFile?: string | string[];
service?: string;
runServices?: string[];
workspaceFolder?: string;
workspaceMount?: string;
forwardPorts?: (number | string)[];
portsAttributes?: Record<string, unknown>;
otherPortsAttributes?: unknown;
features?: Record<string, unknown>;
overrideFeatureInstallOrder?: string[];
customizations?: {
vscode?: {
extensions?: string[];
settings?: Record<string, unknown>;
};
[key: string]: unknown;
};
remoteUser?: string;
containerUser?: string;
remoteEnv?: Record<string, string | null>;
containerEnv?: Record<string, string>;
updateRemoteUserUID?: boolean;
userEnvProbe?: string;
overrideCommand?: boolean;
shutdownAction?: string;
init?: boolean;
privileged?: boolean;
capAdd?: string[];
securityOpt?: string[];
mounts?: unknown[];
onCreateCommand?: string | string[] | Record<string, string | string[]>;
updateContentCommand?: string | string[] | Record<string, string | string[]>;
postCreateCommand?: string | string[] | Record<string, string | string[]>;
postStartCommand?: string | string[] | Record<string, string | string[]>;
postAttachCommand?: string | string[] | Record<string, string | string[]>;
waitFor?: string;
hostRequirements?: {
cpus?: number;
memory?: string;
storage?: string;
gpu?: boolean | string | { cores?: number; memory?: string };
};
}
interface Issue {
severity: "error" | "warning" | "info";
category: string;
message: string;
suggestion?: string;
}
function analyzeConfig(config: DevcontainerConfig, filePath: string): Issue[] {
const issues: Issue[] = [];
// Check for multiple build approaches
const buildApproaches = [
config.image !== undefined,
config.build?.dockerfile !== undefined,
config.dockerComposeFile !== undefined,
].filter(Boolean).length;
if (buildApproaches === 0) {
issues.push({
severity: "error",
category: "Configuration",
message: "No container source specified",
suggestion: "Add 'image', 'build.dockerfile', or 'dockerComposeFile' to define the container source",
});
} else if (buildApproaches > 1) {
issues.push({
severity: "warning",
category: "Configuration",
message: "Multiple build approaches specified (image, dockerfile, dockerComposeFile)",
suggestion: "Use only ONE approach: image for pre-built, build.dockerfile for custom, or dockerComposeFile for multi-service",
});
}
// Check Docker Compose configuration
if (config.dockerComposeFile && !config.service) {
issues.push({
severity: "error",
category: "Docker Compose",
message: "dockerComposeFile specified but no 'service' defined",
suggestion: "Add 'service' to specify which Compose service to attach to",
});
}
// Analyze features
if (config.features) {
const featureCount = Object.keys(config.features).length;
if (featureCount > 10) {
issues.push({
severity: "warning",
category: "Performance",
message: `High number of features (${featureCount}) may slow container startup`,
suggestion: "Review if all features are necessary. Consider consolidating or moving to Dockerfile",
});
}
// Check for potentially conflicting features
const featureKeys = Object.keys(config.features);
const pythonFeatures = featureKeys.filter(f => f.includes("python"));
const nodeFeatures = featureKeys.filter(f => f.includes("node"));
if (pythonFeatures.length > 1) {
issues.push({
severity: "warning",
category: "Features",
message: "Multiple Python-related features detected",
suggestion: "This may cause version conflicts. Use a single Python feature with explicit version",
});
}
if (nodeFeatures.length > 1) {
issues.push({
severity: "warning",
category: "Features",
message: "Multiple Node.js-related features detected",
suggestion: "This may cause version conflicts. Use a single Node feature with explicit version",
});
}
}
// Analyze extensions
const extensions = config.customizations?.vscode?.extensions || [];
if (extensions.length > 30) {
issues.push({
severity: "warning",
category: "Performance",
message: `High number of extensions (${extensions.length}) may slow VS Code startup`,
suggestion: "Review extensions periodically. Remove unused ones. Consider workspace recommendations instead",
});
}
// Check extension format
const malformedExtensions = extensions.filter(ext => !ext.includes("."));
if (malformedExtensions.length > 0) {
issues.push({
severity: "error",
category: "Extensions",
message: `Malformed extension IDs: ${malformedExtensions.join(", ")}`,
suggestion: "Extension IDs should be in format 'publisher.extension-name'",
});
}
// Analyze lifecycle commands
const analyzeCommand = (
name: string,
cmd: string | string[] | Record<string, string | string[]> | undefined
) => {
if (!cmd) return;
const cmdStr = typeof cmd === "string" ? cmd : JSON.stringify(cmd);
// Check for slow operations in postCreateCommand
if (name === "postCreateCommand") {
if (cmdStr.includes("npm install") || cmdStr.includes("yarn install") || cmdStr.includes("pnpm install")) {
issues.push({
severity: "warning",
category: "Performance",
message: "npm/yarn/pnpm install in postCreateCommand runs every container creation",
suggestion: "Move to Dockerfile after COPY package*.json to cache dependency installation",
});
}
if (cmdStr.includes("pip install") && !cmdStr.includes("requirements.txt")) {
issues.push({
severity: "info",
category: "Performance",
message: "pip install in postCreateCommand",
suggestion: "Consider moving to Dockerfile with COPY requirements.txt for caching",
});
}
if (cmdStr.includes("apt-get") || cmdStr.includes("apt ")) {
issues.push({
severity: "warning",
category: "Performance",
message: "apt-get in postCreateCommand runs every container creation",
suggestion: "Move system package installation to Dockerfile for caching",
});
}
}
// Very long commands might indicate complexity
if (cmdStr.length > 500) {
issues.push({
severity: "info",
category: "Maintainability",
message: `${name} is very long (${cmdStr.length} chars)`,
suggestion: "Consider moving to a shell script for readability and maintainability",
});
}
};
analyzeCommand("onCreateCommand", config.onCreateCommand);
analyzeCommand("updateContentCommand", config.updateContentCommand);
analyzeCommand("postCreateCommand", config.postCreateCommand);
analyzeCommand("postStartCommand", config.postStartCommand);
analyzeCommand("postAttachCommand", config.postAttachCommand);
// Check user configuration
if (!config.remoteUser && !config.containerUser) {
issues.push({
severity: "info",
category: "Security",
message: "No remoteUser specified",
suggestion: "Consider setting remoteUser to avoid running as root. Default devcontainer images have 'vscode' user",
});
}
// Check for privileged mode
if (config.privileged) {
issues.push({
severity: "warning",
category: "Security",
message: "Container runs in privileged mode",
suggestion: "Privileged mode grants full host access. Use only if absolutely necessary (e.g., Docker-in-Docker)",
});
}
// Check for port forwarding issues
if (config.forwardPorts && config.forwardPorts.length > 20) {
issues.push({
severity: "warning",
category: "Configuration",
message: `Large number of forwarded ports (${config.forwardPorts.length})`,
suggestion: "Review if all ports need explicit forwarding. VS Code auto-forwards detected ports",
});
}
// Check workspace configuration
if (config.dockerComposeFile && !config.workspaceFolder) {
issues.push({
severity: "warning",
category: "Docker Compose",
message: "Using Docker Compose without explicit workspaceFolder",
suggestion: "Set workspaceFolder to ensure consistent workspace location across environments",
});
}
// Check for missing name
if (!config.name) {
issues.push({
severity: "info",
category: "Configuration",
message: "No 'name' specified for devcontainer",
suggestion: "Add a descriptive name to identify this devcontainer in VS Code",
});
}
// Check for Codespaces compatibility
if (config.mounts && Array.isArray(config.mounts)) {
const hostMounts = config.mounts.filter(m => {
const mountStr = typeof m === "string" ? m : JSON.stringify(m);
return mountStr.includes("/home/") || mountStr.includes("/Users/") || mountStr.includes("C:\\");
});
if (hostMounts.length > 0) {
issues.push({
severity: "warning",
category: "Codespaces",
message: "Host-specific mount paths detected",
suggestion: "Hardcoded host paths will fail in GitHub Codespaces. Use environment variables or conditional mounts",
});
}
}
return issues;
}
function formatIssue(issue: Issue): string {
const severityEmoji = {
error: "❌",
warning: "⚠️",
info: "ℹ️",
};
let output = `${severityEmoji[issue.severity]} [${issue.category}] ${issue.message}`;
if (issue.suggestion) {
output += `\n 💡 ${issue.suggestion}`;
}
return output;
}
function printSummary(issues: Issue[]): void {
const errors = issues.filter(i => i.severity === "error");
const warnings = issues.filter(i => i.severity === "warning");
const infos = issues.filter(i => i.severity === "info");
console.log("\n📊 Summary");
console.log("─".repeat(40));
console.log(` Errors: ${errors.length}`);
console.log(` Warnings: ${warnings.length}`);
console.log(` Info: ${infos.length}`);
if (errors.length === 0 && warnings.length === 0) {
console.log("\n✅ No critical issues found!");
} else if (errors.length > 0) {
console.log("\n🔴 Fix errors before proceeding");
} else {
console.log("\n🟡 Review warnings for potential improvements");
}
}
async function main() {
// Determine file path
let filePath = Deno.args[0];
if (!filePath) {
// Try common locations
const commonPaths = [
".devcontainer/devcontainer.json",
".devcontainer.json",
"devcontainer.json",
];
for (const path of commonPaths) {
try {
await Deno.stat(path);
filePath = path;
break;
} catch {
// File doesn't exist, try next
}
}
}
if (!filePath) {
console.error("❌ No devcontainer.json found");
console.error(" Usage: deno run --allow-read analyze-devcontainer.ts [path]");
console.error(" Or run from a directory containing .devcontainer/devcontainer.json");
Deno.exit(1);
}
console.log(`\n🔍 Analyzing: ${filePath}\n`);
console.log("─".repeat(60));
// Read and parse file
let config: DevcontainerConfig;
try {
const content = await Deno.readTextFile(filePath);
// Remove comments (JSON with comments support)
const cleanContent = content
.replace(/\/\/.*$/gm, "") // Single-line comments
.replace(/\/\*[\s\S]*?\*\//g, ""); // Multi-line comments
config = JSON.parse(cleanContent);
} catch (error) {
if (error instanceof SyntaxError) {
console.error(`❌ Invalid JSON in ${filePath}`);
console.error(` ${error.message}`);
Deno.exit(1);
}
throw error;
}
// Analyze
const issues = analyzeConfig(config, filePath);
// Group by severity
const errors = issues.filter(i => i.severity === "error");
const warnings = issues.filter(i => i.severity === "warning");
const infos = issues.filter(i => i.severity === "info");
// Print issues
if (errors.length > 0) {
console.log("\n🔴 ERRORS\n");
errors.forEach(i => console.log(formatIssue(i) + "\n"));
}
if (warnings.length > 0) {
console.log("\n🟡 WARNINGS\n");
warnings.forEach(i => console.log(formatIssue(i) + "\n"));
}
if (infos.length > 0) {
console.log("\nℹ️ INFO\n");
infos.forEach(i => console.log(formatIssue(i) + "\n"));
}
printSummary(issues);
// Exit with error code if there are errors
if (errors.length > 0) {
Deno.exit(1);
}
}
main();
#!/usr/bin/env -S deno run --allow-run --allow-net
/**
* Container Image Security Scanner
*
* Wraps Trivy for vulnerability scanning of container images.
* Provides human-readable output with remediation suggestions.
*
* Usage:
* deno run --allow-run --allow-net scripts/scan-image.ts <image-name>
*
* Examples:
* deno run --allow-run --allow-net scripts/scan-image.ts node:20-slim
* deno run --allow-run --allow-net scripts/scan-image.ts mcr.microsoft.com/devcontainers/base:ubuntu
*
* Requires Trivy to be installed:
* brew install trivy
* # or
* curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
*/
interface Vulnerability {
VulnerabilityID: string;
PkgName: string;
InstalledVersion: string;
FixedVersion?: string;
Severity: string;
Title?: string;
Description?: string;
PrimaryURL?: string;
}
interface ScanResult {
Target: string;
Type: string;
Vulnerabilities?: Vulnerability[];
}
interface TrivyOutput {
Results?: ScanResult[];
}
const SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"];
const SEVERITY_EMOJI: Record<string, string> = {
CRITICAL: "🔴",
HIGH: "🟠",
MEDIUM: "🟡",
LOW: "🔵",
UNKNOWN: "⚪",
};
async function checkTrivyInstalled(): Promise<boolean> {
try {
const command = new Deno.Command("trivy", {
args: ["--version"],
stdout: "null",
stderr: "null",
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
async function scanImage(imageName: string): Promise<TrivyOutput | null> {
console.log(`\n🔍 Scanning image: ${imageName}\n`);
console.log("This may take a moment for first scan (downloading vulnerability database)...\n");
try {
const command = new Deno.Command("trivy", {
args: [
"image",
"--format", "json",
"--severity", "CRITICAL,HIGH,MEDIUM,LOW",
imageName,
],
stdout: "piped",
stderr: "piped",
});
const { code, stdout, stderr } = await command.output();
if (code !== 0) {
const errorText = new TextDecoder().decode(stderr);
console.error(`❌ Trivy scan failed:\n${errorText}`);
return null;
}
const outputText = new TextDecoder().decode(stdout);
return JSON.parse(outputText);
} catch (error) {
console.error(`❌ Error running Trivy: ${error}`);
return null;
}
}
function summarizeVulnerabilities(results: ScanResult[]): Map<string, Vulnerability[]> {
const bySeverity = new Map<string, Vulnerability[]>();
for (const severity of SEVERITY_ORDER) {
bySeverity.set(severity, []);
}
for (const result of results) {
if (!result.Vulnerabilities) continue;
for (const vuln of result.Vulnerabilities) {
const list = bySeverity.get(vuln.Severity) || [];
list.push(vuln);
bySeverity.set(vuln.Severity, list);
}
}
return bySeverity;
}
function printSummary(bySeverity: Map<string, Vulnerability[]>): void {
console.log("─".repeat(60));
console.log("\n📊 VULNERABILITY SUMMARY\n");
let total = 0;
let fixable = 0;
for (const severity of SEVERITY_ORDER) {
const vulns = bySeverity.get(severity) || [];
if (vulns.length === 0) continue;
const fixableCount = vulns.filter(v => v.FixedVersion).length;
total += vulns.length;
fixable += fixableCount;
console.log(
`${SEVERITY_EMOJI[severity]} ${severity.padEnd(10)} ${vulns.length.toString().padStart(4)} vulnerabilities (${fixableCount} fixable)`
);
}
console.log("─".repeat(40));
console.log(` TOTAL: ${total} vulnerabilities`);
console.log(` FIXABLE: ${fixable} have patches available`);
}
function printDetails(bySeverity: Map<string, Vulnerability[]>, showAll: boolean): void {
console.log("\n📋 VULNERABILITY DETAILS\n");
// Show CRITICAL and HIGH by default, all if requested
const severitiesToShow = showAll
? SEVERITY_ORDER
: ["CRITICAL", "HIGH"];
for (const severity of severitiesToShow) {
const vulns = bySeverity.get(severity) || [];
if (vulns.length === 0) continue;
console.log(`\n${SEVERITY_EMOJI[severity]} ${severity} (${vulns.length})\n`);
console.log("─".repeat(60));
// Group by package
const byPackage = new Map<string, Vulnerability[]>();
for (const vuln of vulns) {
const list = byPackage.get(vuln.PkgName) || [];
list.push(vuln);
byPackage.set(vuln.PkgName, list);
}
for (const [pkg, pkgVulns] of byPackage) {
const firstVuln = pkgVulns[0];
console.log(`\n 📦 ${pkg} (${firstVuln.InstalledVersion})`);
if (firstVuln.FixedVersion) {
console.log(` 💡 Fix: Upgrade to ${firstVuln.FixedVersion}`);
}
for (const vuln of pkgVulns.slice(0, 3)) { // Show max 3 per package
console.log(` • ${vuln.VulnerabilityID}`);
if (vuln.Title) {
const title = vuln.Title.length > 60
? vuln.Title.substring(0, 60) + "..."
: vuln.Title;
console.log(` ${title}`);
}
}
if (pkgVulns.length > 3) {
console.log(` ... and ${pkgVulns.length - 3} more`);
}
}
}
if (!showAll) {
const mediumLow = (bySeverity.get("MEDIUM")?.length || 0) +
(bySeverity.get("LOW")?.length || 0);
if (mediumLow > 0) {
console.log(`\n📝 ${mediumLow} MEDIUM/LOW vulnerabilities not shown. Use --all to see all.`);
}
}
}
function printRecommendations(bySeverity: Map<string, Vulnerability[]>): void {
console.log("\n💡 RECOMMENDATIONS\n");
console.log("─".repeat(60));
const critical = bySeverity.get("CRITICAL") || [];
const high = bySeverity.get("HIGH") || [];
if (critical.length > 0) {
console.log("\n🔴 CRITICAL ISSUES - Address immediately:\n");
// Find packages with critical vulns that have fixes
const fixableCritical = critical.filter(v => v.FixedVersion);
const uniquePackages = [...new Set(fixableCritical.map(v => v.PkgName))];
if (uniquePackages.length > 0) {
console.log(" Update these packages:");
for (const pkg of uniquePackages.slice(0, 5)) {
const vuln = fixableCritical.find(v => v.PkgName === pkg);
if (vuln) {
console.log(` • ${pkg}: ${vuln.InstalledVersion} → ${vuln.FixedVersion}`);
}
}
if (uniquePackages.length > 5) {
console.log(` ... and ${uniquePackages.length - 5} more`);
}
}
const unfixableCritical = critical.filter(v => !v.FixedVersion);
if (unfixableCritical.length > 0) {
console.log(`\n ⚠️ ${unfixableCritical.length} critical vulnerabilities have no fix yet.`);
console.log(" Consider using a different base image or monitoring for patches.");
}
}
if (high.length > 0 && critical.length === 0) {
console.log("\n🟠 HIGH PRIORITY - Plan to address soon:\n");
const fixableHigh = high.filter(v => v.FixedVersion);
console.log(` ${fixableHigh.length} of ${high.length} have available fixes.`);
}
// General recommendations
console.log("\n📝 GENERAL RECOMMENDATIONS:\n");
console.log(" 1. Update base image regularly");
console.log(" Use specific version tags and update periodically");
console.log("");
console.log(" 2. Use minimal base images");
console.log(" Consider: -slim, -alpine, or distroless variants");
console.log("");
console.log(" 3. Multi-stage builds");
console.log(" Don't include build tools in final image");
console.log("");
console.log(" 4. Regular scanning");
console.log(" Integrate trivy into CI/CD pipeline");
}
async function main() {
const args = Deno.args;
const showAll = args.includes("--all");
const imageName = args.filter(a => !a.startsWith("--"))[0];
if (!imageName) {
console.log(`
🔍 Container Image Security Scanner
Usage:
deno run --allow-run --allow-net scan-image.ts <image-name> [--all]
Examples:
scan-image.ts node:20-slim
scan-image.ts mcr.microsoft.com/devcontainers/base:ubuntu
scan-image.ts my-app:latest --all
Options:
--all Show all vulnerabilities (default shows only CRITICAL and HIGH)
Requires Trivy: brew install trivy
`);
Deno.exit(1);
}
// Check if Trivy is installed
const trivyInstalled = await checkTrivyInstalled();
if (!trivyInstalled) {
console.error(`
❌ Trivy not found
Install Trivy to use this scanner:
macOS: brew install trivy
Linux: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
Docker: docker run aquasec/trivy image ${imageName}
More info: https://github.com/aquasecurity/trivy
`);
Deno.exit(1);
}
// Run scan
const output = await scanImage(imageName);
if (!output) {
Deno.exit(1);
}
if (!output.Results || output.Results.length === 0) {
console.log("✅ No vulnerabilities found!");
Deno.exit(0);
}
// Process results
const bySeverity = summarizeVulnerabilities(output.Results);
// Check if any vulnerabilities
let totalVulns = 0;
for (const vulns of bySeverity.values()) {
totalVulns += vulns.length;
}
if (totalVulns === 0) {
console.log("✅ No vulnerabilities found!");
Deno.exit(0);
}
// Print results
printSummary(bySeverity);
printDetails(bySeverity, showAll);
printRecommendations(bySeverity);
// Exit code based on severity
const critical = bySeverity.get("CRITICAL") || [];
const high = bySeverity.get("HIGH") || [];
if (critical.length > 0) {
console.log("\n🔴 CRITICAL vulnerabilities found - exit code 2");
Deno.exit(2);
} else if (high.length > 0) {
console.log("\n🟠 HIGH vulnerabilities found - exit code 1");
Deno.exit(1);
}
console.log("\n✅ No CRITICAL or HIGH vulnerabilities");
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Dockerfile Validator
*
* Validates Dockerfiles for common anti-patterns, security issues,
* and optimization opportunities.
*
* Usage:
* deno run --allow-read scripts/validate-dockerfile.ts [path-to-dockerfile]
*
* If no path provided, searches for Dockerfile or .devcontainer/Dockerfile
*/
interface Issue {
severity: "error" | "warning" | "info";
category: string;
line?: number;
message: string;
suggestion?: string;
}
interface DockerfileInstruction {
line: number;
instruction: string;
args: string;
raw: string;
}
function parseDockerfile(content: string): DockerfileInstruction[] {
const instructions: DockerfileInstruction[] = [];
const lines = content.split("\n");
let currentInstruction = "";
let currentLine = 0;
let instructionStart = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Skip empty lines and comments
if (trimmed === "" || trimmed.startsWith("#")) {
continue;
}
// Handle line continuation
if (currentInstruction) {
currentInstruction += " " + trimmed.replace(/\\$/, "");
if (!trimmed.endsWith("\\")) {
// End of multi-line instruction
const match = currentInstruction.match(/^(\w+)\s+(.*)/s);
if (match) {
instructions.push({
line: instructionStart + 1,
instruction: match[1].toUpperCase(),
args: match[2].trim(),
raw: currentInstruction,
});
}
currentInstruction = "";
}
} else if (trimmed.endsWith("\\")) {
// Start of multi-line instruction
currentInstruction = trimmed.replace(/\\$/, "");
instructionStart = i;
} else {
// Single-line instruction
const match = trimmed.match(/^(\w+)\s*(.*)/);
if (match) {
instructions.push({
line: i + 1,
instruction: match[1].toUpperCase(),
args: match[2].trim(),
raw: trimmed,
});
}
}
}
return instructions;
}
function validateDockerfile(content: string, filePath: string): Issue[] {
const issues: Issue[] = [];
const instructions = parseDockerfile(content);
const lines = content.split("\n");
// Track state
let hasFrom = false;
let lastFromLine = 0;
let hasUser = false;
let copyAllBeforeDeps = false;
let runCount = 0;
let fromCount = 0;
// Check for FROM instruction
const fromInstructions = instructions.filter(i => i.instruction === "FROM");
if (fromInstructions.length === 0) {
issues.push({
severity: "error",
category: "Structure",
message: "No FROM instruction found",
suggestion: "Every Dockerfile must start with a FROM instruction",
});
} else {
hasFrom = true;
fromCount = fromInstructions.length;
lastFromLine = fromInstructions[fromInstructions.length - 1].line;
}
// Analyze each instruction
for (let i = 0; i < instructions.length; i++) {
const inst = instructions[i];
switch (inst.instruction) {
case "FROM": {
// Check for :latest tag
if (inst.args.includes(":latest") || (!inst.args.includes(":") && !inst.args.includes("@"))) {
const tag = inst.args.includes(":latest") ? ":latest" : "no tag (defaults to :latest)";
issues.push({
severity: "warning",
category: "Reproducibility",
line: inst.line,
message: `Base image uses ${tag}`,
suggestion: "Pin to a specific version for reproducible builds (e.g., node:20-slim, python:3.11)",
});
}
// Check for common large base images
const largeImages = ["ubuntu", "debian", "centos", "fedora"];
const baseImage = inst.args.split(":")[0].split("/").pop() || "";
if (largeImages.includes(baseImage.toLowerCase()) && !inst.args.includes("slim")) {
issues.push({
severity: "info",
category: "Optimization",
line: inst.line,
message: `Using full ${baseImage} base image`,
suggestion: "Consider using slim variant or language-specific images (e.g., python:3.11-slim) for smaller size",
});
}
// Check for microsoft devcontainer images (good!)
if (inst.args.includes("mcr.microsoft.com/devcontainers")) {
issues.push({
severity: "info",
category: "Best Practice",
line: inst.line,
message: "Using official devcontainer base image (good!)",
});
}
break;
}
case "RUN": {
runCount++;
// Check for apt-get without cleanup
if (inst.args.includes("apt-get install") && !inst.args.includes("rm -rf /var/lib/apt/lists")) {
issues.push({
severity: "warning",
category: "Optimization",
line: inst.line,
message: "apt-get install without cleaning apt cache",
suggestion: "Add '&& rm -rf /var/lib/apt/lists/*' in the same RUN to reduce layer size",
});
}
// Check for apt-get update in separate RUN
if (inst.args.trim() === "apt-get update" || inst.args.trim().startsWith("apt-get update &&") === false) {
if (inst.args.includes("apt-get update") && !inst.args.includes("apt-get install")) {
issues.push({
severity: "warning",
category: "Optimization",
line: inst.line,
message: "apt-get update in separate RUN from install",
suggestion: "Combine 'apt-get update && apt-get install' in one RUN to ensure fresh package lists",
});
}
}
// Check for pip install without cache disable
if (inst.args.includes("pip install") && !inst.args.includes("--no-cache-dir")) {
issues.push({
severity: "info",
category: "Optimization",
line: inst.line,
message: "pip install without --no-cache-dir",
suggestion: "Add --no-cache-dir to reduce image size",
});
}
// Check for npm install without cache clean
if (inst.args.includes("npm install") && !inst.args.includes("npm cache clean")) {
issues.push({
severity: "info",
category: "Optimization",
line: inst.line,
message: "npm install without cache cleanup",
suggestion: "Consider adding 'npm cache clean --force' if image size is a concern",
});
}
// Check for curl/wget without cleanup
if ((inst.args.includes("curl") || inst.args.includes("wget")) && inst.args.includes("-o")) {
if (!inst.args.includes("rm ")) {
issues.push({
severity: "info",
category: "Optimization",
line: inst.line,
message: "Downloaded file may remain in image",
suggestion: "If downloading and extracting, remove the archive in the same RUN",
});
}
}
break;
}
case "COPY": {
// Check for COPY . . before dependency files
if (inst.args === ". ." || inst.args === ". /app" || inst.args.match(/^\.\s+\/\w+$/)) {
// Check if this is before package.json/requirements.txt copies
const laterCopies = instructions.slice(i + 1).filter(x => x.instruction === "COPY");
const depFileCopies = laterCopies.filter(c =>
c.args.includes("package") ||
c.args.includes("requirements") ||
c.args.includes("Gemfile") ||
c.args.includes("go.mod") ||
c.args.includes("Cargo.toml")
);
if (depFileCopies.length === 0) {
// Check if dep files are copied BEFORE this COPY . .
const earlierCopies = instructions.slice(0, i).filter(x => x.instruction === "COPY");
const earlierDepCopies = earlierCopies.filter(c =>
c.args.includes("package") ||
c.args.includes("requirements") ||
c.args.includes("Gemfile") ||
c.args.includes("go.mod") ||
c.args.includes("Cargo.toml")
);
if (earlierDepCopies.length === 0) {
issues.push({
severity: "warning",
category: "Caching",
line: inst.line,
message: "'COPY . .' without prior dependency file copy",
suggestion: "Copy package.json/requirements.txt first, install deps, then COPY . . for better cache utilization",
});
copyAllBeforeDeps = true;
}
}
}
break;
}
case "ADD": {
// ADD is often unnecessary
if (!inst.args.includes("http") && !inst.args.includes(".tar") && !inst.args.includes(".gz")) {
issues.push({
severity: "info",
category: "Best Practice",
line: inst.line,
message: "ADD used instead of COPY",
suggestion: "Prefer COPY unless you need ADD's auto-extraction or URL features",
});
}
break;
}
case "USER": {
hasUser = true;
if (inst.args.toLowerCase() === "root") {
// Check if there are commands after this that need root
const laterInstructions = instructions.slice(i + 1);
const hasLaterUserSwitch = laterInstructions.some(x => x.instruction === "USER" && x.args.toLowerCase() !== "root");
if (!hasLaterUserSwitch) {
issues.push({
severity: "warning",
category: "Security",
line: inst.line,
message: "Container may run as root",
suggestion: "Switch to a non-root user before the final CMD/ENTRYPOINT",
});
}
}
break;
}
case "ENV": {
// Check for sensitive-looking environment variables
const sensitivePatterns = ["password", "secret", "key", "token", "credential", "api_key"];
const argsLower = inst.args.toLowerCase();
if (sensitivePatterns.some(p => argsLower.includes(p))) {
issues.push({
severity: "warning",
category: "Security",
line: inst.line,
message: "Potentially sensitive value in ENV",
suggestion: "Don't bake secrets into images. Use runtime environment variables or secrets management",
});
}
break;
}
case "EXPOSE": {
// Just informational
break;
}
case "WORKDIR": {
// Good practice
break;
}
case "CMD":
case "ENTRYPOINT": {
// Check for shell form vs exec form
if (!inst.args.startsWith("[")) {
issues.push({
severity: "info",
category: "Best Practice",
line: inst.line,
message: `${inst.instruction} uses shell form`,
suggestion: "Consider exec form (JSON array) for proper signal handling: [\"executable\", \"param1\"]",
});
}
break;
}
case "HEALTHCHECK": {
// Good practice
issues.push({
severity: "info",
category: "Best Practice",
line: inst.line,
message: "HEALTHCHECK defined (good!)",
});
break;
}
}
}
// Global checks
// No USER instruction
if (!hasUser && hasFrom) {
issues.push({
severity: "warning",
category: "Security",
message: "No USER instruction - container will run as root",
suggestion: "Add 'USER <username>' to run as non-root. Devcontainer images have 'vscode' user available",
});
}
// Too many RUN instructions
if (runCount > 10) {
issues.push({
severity: "info",
category: "Optimization",
message: `High number of RUN instructions (${runCount})`,
suggestion: "Consider combining related RUN commands with && to reduce layers",
});
}
// Multi-stage build detection
if (fromCount > 1) {
issues.push({
severity: "info",
category: "Best Practice",
message: `Multi-stage build detected (${fromCount} stages)`,
});
}
// Check for .dockerignore mention
// This is a heuristic - we can't actually check for the file
issues.push({
severity: "info",
category: "Reminder",
message: "Ensure .dockerignore exists",
suggestion: "Exclude node_modules, .git, build artifacts from the build context",
});
return issues;
}
function formatIssue(issue: Issue): string {
const severityEmoji = {
error: "❌",
warning: "⚠️",
info: "ℹ️",
};
let output = `${severityEmoji[issue.severity]} [${issue.category}]`;
if (issue.line) {
output += ` Line ${issue.line}:`;
}
output += ` ${issue.message}`;
if (issue.suggestion) {
output += `\n 💡 ${issue.suggestion}`;
}
return output;
}
function printSummary(issues: Issue[]): void {
const errors = issues.filter(i => i.severity === "error");
const warnings = issues.filter(i => i.severity === "warning");
const infos = issues.filter(i => i.severity === "info");
console.log("\n📊 Summary");
console.log("─".repeat(40));
console.log(` Errors: ${errors.length}`);
console.log(` Warnings: ${warnings.length}`);
console.log(` Info: ${infos.length}`);
if (errors.length === 0 && warnings.length === 0) {
console.log("\n✅ No critical issues found!");
} else if (errors.length > 0) {
console.log("\n🔴 Fix errors before building");
} else {
console.log("\n🟡 Review warnings for potential improvements");
}
}
async function main() {
// Determine file path
let filePath = Deno.args[0];
if (!filePath) {
// Try common locations
const commonPaths = [
"Dockerfile",
".devcontainer/Dockerfile",
"docker/Dockerfile",
];
for (const path of commonPaths) {
try {
await Deno.stat(path);
filePath = path;
break;
} catch {
// File doesn't exist, try next
}
}
}
if (!filePath) {
console.error("❌ No Dockerfile found");
console.error(" Usage: deno run --allow-read validate-dockerfile.ts [path]");
console.error(" Or run from a directory containing Dockerfile");
Deno.exit(1);
}
console.log(`\n🔍 Validating: ${filePath}\n`);
console.log("─".repeat(60));
// Read file
let content: string;
try {
content = await Deno.readTextFile(filePath);
} catch (error) {
console.error(`❌ Cannot read ${filePath}`);
console.error(` ${error}`);
Deno.exit(1);
}
// Validate
const issues = validateDockerfile(content, filePath);
// Group by severity
const errors = issues.filter(i => i.severity === "error");
const warnings = issues.filter(i => i.severity === "warning");
const infos = issues.filter(i => i.severity === "info");
// Print issues
if (errors.length > 0) {
console.log("\n🔴 ERRORS\n");
errors.forEach(i => console.log(formatIssue(i) + "\n"));
}
if (warnings.length > 0) {
console.log("\n🟡 WARNINGS\n");
warnings.forEach(i => console.log(formatIssue(i) + "\n"));
}
if (infos.length > 0) {
console.log("\nℹ️ INFO\n");
infos.forEach(i => console.log(formatIssue(i) + "\n"));
}
printSummary(issues);
// Exit with error code if there are errors
if (errors.length > 0) {
Deno.exit(1);
}
}
main();
Related skills
FAQ
What does the devcontainer skill configure?
The devcontainer skill configures devcontainer.json and related Docker settings so Claude-assisted development runs in a reproducible, dependency-complete container shared across machines and contributors.
When should developers use the devcontainer skill?
Use the devcontainer skill when local setups drift, onboarding is inconsistent, or you want Cursor or Claude Code to run inside a standardized container with the same runtimes and tools as CI.