> ## Documentation Index
> Fetch the complete documentation index at: https://ona.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Lab 2: Configuration as Code

> Master Dev Containers and Automations for reproducible environments

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.

<Note>
  If you selected the [ona-samples/workshop](https://github.com/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.
</Note>

## 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

```mermaid theme={null}
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#ffffff','primaryTextColor':'#000000','primaryBorderColor':'#000000','lineColor':'#000000','secondaryColor':'#ffffff','tertiaryColor':'#ffffff'}}}%%
graph LR
    A[devcontainer.json] --> B[Base Image or Dockerfile]
    B --> C[Add Features]
    C --> D[Add IDE Extensions]
    D --> E[Add Lifecycle Commands]
    E --> F[Ready Environment]
```

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:

```json theme={null}
{
  "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 VS Code extensions and settings to enable format-on-save:
   ```json theme={null}
   "customizations": {
     "vscode": {
       "extensions": [
         "dbaeumer.vscode-eslint",
         "esbenp.prettier-vscode"
       ],
       "settings": {
         "editor.formatOnSave": true,
         "editor.defaultFormatter": "esbenp.prettier-vscode"
       }
     }
   }
   ```
3. Save the file
4. 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 `ona environment devcontainer rebuild`
   * **Browser IDE**: Stop and restart the environment from the [Ona dashboard](https://app.ona.com)
5. Once reconnected, verify the extensions are installed in the Extensions sidebar

Browse more features at [containers.dev/features](https://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:

```yaml theme={null}
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:

```yaml theme={null}
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:

```bash theme={null}
ona automations update
```

Check the status:

```bash theme={null}
ona 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:

```yaml theme={null}
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 `ona 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:

```markdown theme={null}
# 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](https://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: `ona 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: `ona 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
* Ona Agent loads this file automatically at the start of every session. If you've updated `AGENTS.md` mid-session, reset the session: click the session name at the top to open the session menu, then select **Reset session**
* 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](/workshops/optional-examples)**:

* 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](/workshops/lab-3-agents)
