---
title: Boot time env
description: Read the env file ops renders for a service, in process, before anything else.
order: 1
---

Feature: `env`.

Ops renders a service's secret group into one env file when it deploys, and hands it to
the container as a secret mounted at `/run/secrets/<name>`. It is never put into the
process environment by ops. `env::load()` reads that file into the process environment,
so the rest of your code reads plain variables with `std::env::var`.

```rust
fn main() {
    ops_sdk::env::load().expect("the deploy env file");
    let url = std::env::var("DATABASE_URL").expect("DATABASE_URL");
    // ...
}
```

Call it as the very first line of `main`, before a runtime starts or a thread reads a
variable.

## What it reads

| situation | what `load()` does |
| --- | --- |
| `ENV_SECRET` is set (a deployed service) | reads `/run/secrets/$ENV_SECRET`. A missing or broken file is an error |
| `ENV_SECRET` is unset or empty (your machine, tests) | reads `.env` in the working directory if there is one. A missing file is fine |

`SECRETS_DIR` moves the directory the secret is read from, for a setup that mounts
secrets somewhere other than `/run/secrets`.

A variable that is already set in the process wins over the file. That is how a plain
container variable overrides a group key of the same name.

## Treat the error as fatal

`load()` returns `EnvFileError` when the deploy file is missing or does not parse. Stop
the process: a service that boots with half its configuration silently absent is worse
than one that does not boot. The error names the file. A file that does not parse is
reported with the line it could not read, value included, so the boot log of a service
whose env file is broken can hold a secret. Rotate that variable once the file is fixed.

## Databases

A Database resource writes its connection into the secret group it names, so after
`load()` it is an ordinary variable. For the default key `DATABASE_URL`:

| engine | what the group holds |
| --- | --- |
| postgres | `DATABASE_URL=postgres://<login>:<password>@<server>:5432/<database>` |
| mariadb | `DATABASE_URL=mysql://<login>:<password>@<server>:3306/<database>` |
| libsql | `DATABASE_URL`, plus `DATABASE_URL_HTTP_URL` and `DATABASE_URL_AUTH_TOKEN` for clients that take the address and the token apart |
| sqlite | no group key: the service that mounts the database gets `DATABASE_URL=sqlite://<path>/<name>.sqlite` as a plain container variable |

The server names resolve only inside the project's own environment, so these addresses
never connect from your machine. Run a local database there and point your own `.env` at
it.

A rotation rewrites the key in the group and restarts the service, so reading the
variable once at boot is enough.
