Skip to main content
In Lab 1, you learned what Ona Environments are. Now you’ll learn how to configure them precisely for your project’s needs using Dev Containers and Automations.
If you selected the ona-samples/workshop repo (recommended for first-timers), this lab builds on the configuration files already in that repository. The examples below reference that project’s structure.If you’re using your own repository, use the examples as a guide and adapt them to your project’s stack and needs.

Why configuration as code matters

Traditional development setup is manual and fragile:
  • “Install Node 18, then PostgreSQL, then Redis…”
  • Works differently on Mac vs Windows vs Linux
  • Drifts over time as people install different versions
  • Takes hours for new team members
With Ona, you declare what you need in two files:
  • .devcontainer/devcontainer.json: Your tools and environment
  • .ona/automations.yaml: Your tasks and services
Everyone gets the same setup. Automatically. Every time.

Dev Containers: The foundation

How Dev Containers work

A Dev Container starts with a base image or Dockerfile, adds features (like Docker or Python), configures your IDE, and runs commands to complete setup.

Understanding your Dev Container

Open .devcontainer/devcontainer.json in your IDE. If you’re using the ona-samples/workshop repo, it looks like this:
{
  "name": "Portfolio Manager Workshop",
  "build": {
    "context": ".",
    "dockerfile": "Dockerfile"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ]
    }
  },
  "remoteUser": "gitpod",
  "containerUser": "gitpod",
  "forwardPorts": [3000, 3001, 5432],
  "portsAttributes": {
    "3000": { "label": "Frontend", "onAutoForward": "notify" },
    "3001": { "label": "Backend API", "onAutoForward": "notify" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "ignore" }
  }
}
Let’s break down each section:
  • build: Instead of a pre-built image, this uses a Dockerfile in the same directory. This gives you full control over the base environment. The Dockerfile in the workshop repo uses gitpod/workspace-full, which includes Node.js and common development tools.
  • customizations.vscode.extensions: IDE extensions installed automatically. Here, ESLint and Prettier are included so every developer gets consistent linting and formatting.
  • remoteUser / containerUser: The user identity inside the container. Using gitpod ensures correct file permissions.
  • forwardPorts: Ports that are automatically forwarded from the environment to your IDE. Port 3000 is the frontend, 3001 is the backend API, and 5432 is for PostgreSQL.
  • portsAttributes: Controls how forwarded ports behave (labels, auto-forward behavior).

Try it: Customize your Dev Container

Let’s add a useful tool to your environment. We’ll add the GitHub CLI as a Dev Container feature.
  1. Open .devcontainer/devcontainer.json
  2. Add a features section (if one doesn’t exist):
    "features": {
      "ghcr.io/devcontainers/features/github-cli:1": {}
    }
    
  3. You can also add VS Code settings to enable format-on-save:
    "customizations": {
      "vscode": {
        "extensions": [
          "dbaeumer.vscode-eslint",
          "esbenp.prettier-vscode"
        ],
        "settings": {
          "editor.formatOnSave": true,
          "editor.defaultFormatter": "esbenp.prettier-vscode"
        }
      }
    }
    
  4. Save the file
  5. Rebuild the container to apply your changes:
    • VS Code / Cursor: Open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and run Ona: Rebuild Container
    • Terminal: Run gitpod environment devcontainer rebuild
    • Browser IDE: Stop and restart the environment from the Ona dashboard
  6. Verify: run gh --version in the terminal
Browse more features at containers.dev/features. Features install automatically, no manual steps, no forgotten dependencies.

Automations: Tasks and services

Dev Containers handle tools. Automations handle what runs in your environment.

The two types of automations

Tasks: One-time commands (install dependencies, run migrations, seed data) Services: Long-running processes (databases, backend servers, dev servers)

Understanding your automations

Open .ona/automations.yaml in your IDE. If you’re using the ona-samples/workshop repo, it contains a backend service:
services:
  backend:
    name: Backend API
    description: Node.js Express API server
    triggeredBy:
      - postDevcontainerStart
    commands:
      start: |
        cd /workspaces/workshop/backend
        echo "Installing backend dependencies..."
        npm install
        echo "Initializing SQLite database..."
        node init-db.js
        echo "Starting backend API..."
        npm start
      ready: |
        curl -f -s http://localhost:3001/api/health > /dev/null
Here’s what each part does:
  • services: Long-running processes that stay active in the background. The backend service runs the Express API server.
  • triggeredBy: postDevcontainerStart: The service starts automatically after the Dev Container is ready.
  • commands.start: The sequence of commands to run: install dependencies, initialize the database, and start the server.
  • commands.ready: A health check that confirms the service is running. Ona uses this to determine when dependent services can start.
When you open this environment, the backend API starts automatically. No manual npm install, no manual node server.js.

Try it: Add a frontend service

The workshop repo has a frontend app, but it’s not configured as an automation yet. Let’s add it. Open .ona/automations.yaml and add a frontend service:
services:
  backend:
    name: Backend API
    description: Node.js Express API server
    triggeredBy:
      - postDevcontainerStart
    commands:
      start: |
        cd /workspaces/workshop/backend
        echo "Installing backend dependencies..."
        npm install
        echo "Initializing SQLite database..."
        node init-db.js
        echo "Starting backend API..."
        npm start
      ready: |
        curl -f -s http://localhost:3001/api/health > /dev/null

  frontend:
    name: Frontend Dev Server
    description: React + Vite development server
    triggeredBy:
      - postDevcontainerStart
    commands:
      start: |
        cd /workspaces/workshop/frontend
        echo "Installing frontend dependencies..."
        npm install
        echo "Starting frontend dev server..."
        npm run dev
      ready: |
        curl -f -s http://localhost:3000 > /dev/null
After saving, apply the change:
gitpod automations update
Check the status:
gitpod automations service list
You should see both backend and frontend services running. The frontend dev server is now available on port 3000.

Advanced: Service dependencies and tasks

Services can depend on each other, and tasks handle one-time operations. Here’s an example that extends the workshop setup:
services:
  backend:
    name: Backend API
    description: Node.js Express API server
    triggeredBy:
      - postDevcontainerStart
    commands:
      start: |
        cd /workspaces/workshop/backend
        npm install
        node init-db.js
        npm start
      ready: curl -f -s http://localhost:3001/api/health > /dev/null

  frontend:
    name: Frontend Dev Server
    description: React + Vite development server
    triggeredBy:
      - postDevcontainerStart
    commands:
      start: |
        cd /workspaces/workshop/frontend
        npm install
        npm run dev
      ready: curl -f -s http://localhost:3000 > /dev/null

tasks:
  seed-data:
    name: Seed Sample Data
    description: Add sample portfolios and transactions
    triggeredBy:
      - manual
    command: |
      curl -s -X POST http://localhost:3001/api/portfolios \
        -H "Content-Type: application/json" \
        -d '{"name": "Tech Portfolio", "description": "Technology stocks"}'
      echo "Sample data seeded!"
Key concepts:
  • Services: Long-running processes with commands.start and an optional commands.ready health check. The ready check confirms the service is available before dependent tasks run.
  • Tasks: One-time commands with a single command field. The seed-data task adds sample data to the database. Run it with gitpod automations task start seed-data.

AGENTS.md: Teaching agents about your project

Ona Agents can read code, but they don’t know your team’s conventions. AGENTS.md teaches them how your project works, what patterns to follow, and how to validate their changes.

What goes in AGENTS.md

Create AGENTS.md in your repository root. Here’s an example based on the workshop project:
# Project Context for Ona Agents

## Architecture

This is a full-stack portfolio management app:
- Frontend: React + Vite (port 3000)
- Backend: Node.js + Express (port 3001)
- Database: SQLite via better-sqlite3

## Project Structure

- `frontend/src/components/`: React components
- `backend/server.js`: Express API server
- `backend/init-db.js`: Database initialization

## Code Conventions

- Use functional React components with hooks
- API endpoints follow REST conventions under `/api/`
- Keep components small and focused

## Common Tasks

**Adding a new API endpoint:**
1. Add the route in `backend/server.js`
2. Test with `curl http://localhost:3001/api/<endpoint>`

**Adding a new React component:**
1. Create the component in `frontend/src/components/`
2. Import and use it in `App.jsx`

## Before You Push

- [ ] Test the backend: `curl http://localhost:3001/api/health`
- [ ] Check the frontend loads at port 3000
When an agent reads this, it knows:
  • Your architecture and framework choices
  • Where to find and place code
  • How to test and validate changes
  • Your workflow for common tasks

Try it: Create AGENTS.md for your project

  1. Create AGENTS.md in your repository root
  2. Add sections describing:
    • What the project is (tech stack, architecture)
    • Project structure (where things live)
    • Coding conventions (patterns, naming)
    • Common tasks agents might help with
  3. Keep it concise, 2-3 short bullet points per section
  4. Commit and push it
Next time you use an agent, it will automatically read this file and follow your conventions.

Troubleshooting

Dev Container won’t build
  • Validate JSON syntax: Copy your config into jsonlint.com
  • Try a minimal config first, then add features incrementally
  • Check feature compatibility: Some features require specific base images
Automation not starting
  • Check YAML syntax (whitespace matters!)
  • View logs: gitpod automations service logs <name>
  • Ensure Docker is available if your automation uses it (add docker-in-docker feature)
  • Check that commands.ready succeeds; test the command manually in your terminal
Port not accessible
  • Add port to forwardPorts in devcontainer.json: "forwardPorts": [3000, 5432]
  • Or use CLI: gitpod environment port open 3000
  • Check if service is actually listening: netstat -tulpn | grep <port>
AGENTS.md not working
  • File must be named exactly AGENTS.md in repository root
  • Must be committed to Git (agents read from repository)
  • Keep it focused - agents have context limits
Automation order wrong
  • Tasks support dependsOn to control execution order between tasks
  • Services wait for the commands.ready check to pass before dependent tasks run
  • Services wait for the commands.ready check to pass before dependents start
  • Check that your ready command actually succeeds: test it manually in the terminal

What you’ve learned

You now know how to:
  • Configure Dev Containers with base images, features, and IDE settings
  • Create Automations for tasks (one-time commands) and services (long-running processes)
  • Manage dependencies between services for correct startup order
  • Write AGENTS.md to teach agents your project conventions
  • Troubleshoot configuration issues
Your environments are now fully reproducible. Every team member gets the same setup. Automatically.

Need More Examples?

For a comprehensive library of ready-to-use configurations, see Examples Library:
  • Multiple Dev Container configurations for different stacks (Node, Python, Go, Rust, React)
  • Complete Automation patterns (full-stack, monorepo, microservices, testing)
  • AGENTS.md templates for common frameworks
  • Slash command examples
  • Complete workflow patterns
All examples include inline code with one-click copy buttons!
Next: Lab 3: Agents in Action