# Environment and Tasks

> Cordium documentation. Canonical page: <https://octelium.com/docs/cordium/latest/workspaces/runtime>.

The `spec.runtime` field controls the environment variables, lifecycle tasks and the container behavior of a *Workspace*.

## Environment Variables

Environment variables are injected into the *Workspace* container and are therefore available to its lifecycle tasks, terminals, `cordium exec` commands and SSH sessions. Values can be static strings or resolved from the *Secrets* of the *Workspace*'s *Space*, which are referred to by their full name:

```yaml
spec:
  runtime:
    envVars:
      - key: NODE_ENV
        value: development
      - key: LOG_LEVEL
        value: debug
      - key: DATABASE_URL
        fromSecret: staging-database-url.payments.cordium
      - key: STRIPE_SECRET_KEY
        fromSecret: stripe-test-key.payments.cordium
```

If the same key is defined at several levels, the most specific level wins (i.e. *Workspace* > *Template* > *Space* > *UserConfig*). You can also set them via the CLI:

```bash
cordium run --template api-dev.payments.cordium \
  -e LOG_LEVEL=debug \
  --env-from-secret DATABASE_URL=staging-database-url.payments.cordium
```

> **Note:**
>
> A *Secret*-sourced environment variable is readable by every process running inside the *Workspace*, including AI agents. Whenever the credential protects a resource that can be exposed as an Octelium *Service* (e.g. an HTTP API, an LLM provider, a database or an SSH server), prefer secretless access, which keeps the credential entirely out of the *Workspace* (read more [here](https://octelium.com/docs/cordium/latest/workspaces/secretless.md)).

## Lifecycle Tasks

Tasks are shell commands that run at defined points of the *Workspace* lifecycle. Every task supports the following fields:

| Field          | Description                                                                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`         | A name for the task, used in the logs and in failure reports.                                                                                                |
| `run`          | The script to execute via the *Workspace* user's login shell (e.g. `bash`, `zsh` or `sh`). Multi-line scripts are supported.                                 |
| `type`         | `ON_CREATE`, `POST_START` or `PRE_STOP`. Required.                                                                                                           |
| `workingDir`   | The working directory (e.g. `/workspace/repo`). It is recommended to always set it explicitly.                                                               |
| `isBackground` | Starts the task without waiting for it to complete (e.g. dev servers and daemons).                                                                           |
| `runAsRoot`    | Runs the task as `root` instead of the *Workspace* user.                                                                                                     |
| `onFailure`    | `ON_FAILURE_ABORT` fails the *Workspace* run if the task fails, while `ON_FAILURE_CONTINUE` (the default) logs the failure and continues the initialization. |
| `envVars`      | Task-specific environment variables (`key` and `value`), merged with the *Workspace*'s.                                                                      |

### Task Types

**`ON_CREATE`** tasks run on fresh runs only, i.e. the first run of a persistent *Workspace* and every run of an ephemeral one. They do not run again on the subsequent starts of a persistent *Workspace*, nor in *Workspaces* that are restored from a *Template* pre-build or a *WorkspaceSnapshot*, since their effects are already part of the restored storage. Use them for one-time setup: installing dependencies, compiling, running database migrations and seeding data.

**`POST_START`** tasks run on every start of the *Workspace*, after the `ON_CREATE` tasks. Use them for starting background services and dev servers, and for any work that needs secretless access to Octelium *Services* (see below).

**`PRE_STOP`** tasks run when the *Workspace* is stopping, before its container is stopped, and they must complete within 10 minutes. Use them for graceful shutdowns, flushing buffers or uploading results.

Here is an example:

```yaml
spec:
  runtime:
    tasks:
      - name: install-deps
        type: ON_CREATE
        workingDir: /workspace/repo
        run: npm ci
        onFailure: ON_FAILURE_ABORT

      - name: migrate
        type: POST_START
        workingDir: /workspace/repo
        run: npm run db:migrate
        onFailure: ON_FAILURE_ABORT
        envVars:
          - key: DATABASE_URL
            value: postgres://pg-staging/app

      - name: redis
        type: POST_START
        runAsRoot: true
        run: |
          apt-get update && apt-get install -y redis-server
          redis-server --daemonize yes

      - name: dev-server
        type: POST_START
        workingDir: /workspace/repo
        run: npm run dev -- --host 0.0.0.0
        isBackground: true

      - name: graceful-shutdown
        type: PRE_STOP
        workingDir: /workspace/repo
        run: npm run shutdown
```

### Task Order

Within each task type, the foreground tasks run sequentially in the following order: the *Template*'s tasks, the *Workspace*'s tasks, the repository configuration file's tasks, the *UserConfig*'s tasks and finally the *Space*'s tasks. Background tasks are started in the same order but are not waited for. The *Workspace* becomes `RUNNING` once all the `ON_CREATE` and foreground `POST_START` tasks complete.

> **Note:**
>
> Every *Workspace* runs `octelium connect` as the very first `POST_START` task in the background, which is what provides its secretless access to Octelium *Services*. Therefore, tasks that access Octelium *Services* (e.g. a migration against a database *Service* or an AI agent that uses an LLM *Service*) must be `POST_START` tasks. Since the connection is established asynchronously, such tasks should wait for the *Service* to become reachable first, for example:
>
> ```bash
> timeout 120 sh -c 'until curl -s -o /dev/null http://my-api; do sleep 2; done'
> ```

### Timeouts and Failures

All the foreground `ON_CREATE` and `POST_START` tasks of a run must complete within 60 minutes on fresh runs and within 20 minutes on the subsequent runs of a persistent *Workspace*. Longer jobs (e.g. long-running AI agent sessions) should run as background tasks.

When a task with `onFailure: ON_FAILURE_ABORT` fails, the *Workspace* run fails with a `Task` failure that reports the task's name and exit code, and the *Workspace* is stopped. You can inspect the task output via `cordium logs` and the web portal. Tasks without `ON_FAILURE_ABORT` only log their failures. Since scripts run via the *Workspace* user's login shell, which is `sh` in some images, write portable scripts or explicitly invoke `bash -c` when you need Bash-specific features.

## Auto-Stop

Setting `autoStop: true` causes the *Workspace* to stop automatically as soon as all of its `ON_CREATE` and foreground `POST_START` tasks complete. Background tasks are not waited for. This is designed for CI/CD runs, batch jobs and unattended AI agent runs:

```yaml
spec:
  isEphemeral: true
  repository:
    url: https://github.com/acme-corp/monorepo
  runtime:
    autoStop: true
    tasks:
      - name: build
        type: ON_CREATE
        workingDir: /workspace/repo
        run: make build
        onFailure: ON_FAILURE_ABORT
      - name: test
        type: ON_CREATE
        workingDir: /workspace/repo
        run: make test
        onFailure: ON_FAILURE_ABORT
```

You can wait for such a *Workspace* to finish via the SDKs' `WaitUntilStopped`/`wait_until_stopped()` helpers, which also report whether the run failed. For jobs that may exceed the foreground task timeout, run the job as a background task that stops its own *Workspace* once done via `cordium stop`, which stops the current *Workspace* when no name is given:

```yaml
spec:
  runtime:
    tasks:
      - name: long-job
        type: POST_START
        isBackground: true
        workingDir: /workspace/repo
        run: |
          ./scripts/nightly-benchmarks.sh > /workspace/benchmarks.log 2>&1
          cordium stop
```

## Container Command and Entrypoint

By default, the *Workspace* container keeps the image's entrypoint and runs `sleep infinity` as its command, with a minimal init process as PID 1 that reaps zombie processes. You can override them as follows:

```yaml
spec:
  runtime:
    entrypoint: /usr/bin/tini --
    cmd: sleep infinity
    disableInit: true
```

Only set `disableInit: true` if the image already includes its own init system.

## Filesystem and Capabilities

You can make the container's root filesystem read-only. `/workspace`, the home directory, `/tmp`, `/var/tmp` and mounted *Volumes* stay writable:

```yaml
spec:
  runtime:
    filesystem:
      readOnly: true
```

You can also add and drop Linux capabilities. They are merged with the capabilities set at the *Space* and `ClusterConfig` levels:

```yaml
spec:
  runtime:
    capabilities:
      add:
        - SYS_PTRACE
      drop:
        - NET_RAW
```

Or via the CLI:

```bash
cordium run --image python:3.13-slim --read-only --cap-drop NET_RAW --cap-add SYS_PTRACE
```

> **Note:**
>
> Capabilities only apply inside the *Workspace*'s own user namespace, which is mapped to an unprivileged user on the host. In other words, even `CAP_SYS_ADMIN` inside a *Workspace* does not grant any privilege outside of it.

## Nested Containers

*Workspaces* can run their own containers via rootless Podman, which is useful for running databases and other dependencies of integration tests, or for building and pushing container images. Install Podman (e.g. `apt-get install -y podman`) via the image or a task, and use it as root via `sudo` (read the complete example [here](https://octelium.com/docs/cordium/latest/examples/dev/containers.md)):

```bash
sudo podman run -d --name postgres --net host \
  -e POSTGRES_PASSWORD=password docker.io/library/postgres:18
```
