---
title: "Add a database"
summary: "How to add Postgres, Redis or another built-in driver, wire the connection into your application's own variable names, and decide when to use a daemon workload instead."
description: "Declare a supporting service and wire its connection into a workload."
status: shipped
read_when:
  - "Adding a database, cache or queue to a project"
  - "Renaming connection variables for an application that expects its own names"
  - "Deciding between a managed service and a daemon workload"
---

## Declare it

```yaml
services:
  postgres: 17
```

That is sufficient as a declaration. Onebox supplies the image, a durable volume,
a health check, a credential generated on the server, and the connection details
your application reads. You supply no password, no port, and no data path.

PostgreSQL gets a 1 GiB `/dev/shm` ceiling for parallel queries; with a
positive `resources.memory` limit, it gets one quarter of that limit, bounded
between 256 MiB and 1 GiB, but never more than half the limit. A zero limit means
unlimited. This tmpfs uses memory only as PostgreSQL writes to it, and those
pages count against the container's memory limit.

Managed services get up to 2 minutes to shut down cleanly. The first
`ob service apply` after upgrading recreates every service to install this
shutdown window; deploy does so for PostgreSQL services with declared extensions.

Declaring it does not start it. Services live outside the release lifecycle, so
`ob deploy` wires up connections. For services with declared extensions, deploy
also converges their configuration and recreates containers when it changes.
Apply a service explicitly with:

```sh
ob service apply --output ndjson
```

A running PostgreSQL service keeps its current `/dev/shm` ceiling until
`ob service apply` recreates it, or deploy does for services with declared extensions.

On a host you have not bootstrapped yet, `ob bootstrap` does this for you. If you
skip it on a live application, the next deploy stops at preflight with
`service "postgres" is not running — start it with ob bootstrap or ob service apply`.

> **Mount changes are refused by default**
>
> If applying a service would detach or replace a volume that holds data, it stops. Passing `--allow-destructive-mounts` says you have
> accepted that. A major version transition has no override; perform it as a migration.

The service's name is its driver unless `driver` says otherwise, so a second
Postgres is:

```yaml
services:
  postgres: 17
  events: {driver: postgres, version: 17}
  cache:  {driver: redis, version: "7.4"}
```

## Install PostgreSQL extensions

PostgreSQL 18 uses `ghcr.io/labstack/onebox-postgres:18`. No optional extension
is installed by default. Declare the extensions the application requires and
Onebox installs them in its managed database before application migrations run:

```yaml
services:
  database:
    driver: postgres
    version: 18
    features:
      extensions:
        pg_trgm: {}
        vector: {}
```

The application does not need a separate `CREATE EXTENSION` migration. Onebox
creates only missing extensions; it never upgrades or removes an installed
extension during an ordinary apply. An extension absent from the selected image
is reported before application migrations start. A deploy automatically
converges a database with declared extensions before running migration jobs.

The PostgreSQL 18 image includes the common built-in extensions plus a curated
2026 set: pgvector, pgvectorscale, PostGIS, pg_cron, pgAudit, pg_repack,
pg_partman, and HypoPG. Onebox automatically configures required preload
libraries for pg_cron, pgAudit, and `pg_stat_statements`. Optional PostGIS
extensions such as `postgis_topology` are available through the same declaration.

Use the PostgreSQL extension name in the declaration. For example, the pgvector
project provides `vector`, while pgvectorscale provides `vectorscale`. Declaring
`vectorscale` automatically establishes its `vector` dependency:

```yaml
features:
  extensions:
    vectorscale: {}
```

TimescaleDB is not bundled in this first distribution. Its Apache-only package
currently creates a maintenance job that repeatedly fails, while its Community
package has a license unsuitable for a general customer database image. Use a
dedicated TimescaleDB service until the upstream Apache package is clean.

An existing backup-protected PostgreSQL 18 service may still be pinned to the
older official image. To migrate it: disable backup while the old
project file is still active, add the extension declaration and deploy, then
enable backup again:

```console
ob backup disable database --confirm database
# add features.extensions to onebox.yml
ob deploy
ob backup enable database
```

The off-host backup repository is retained throughout this one-time image
transition.

Eleven drivers are supported: `postgres`, `mysql`, `mariadb`, `redis`, `valkey`,
`mongodb`, `rabbitmq`, `minio`, `meilisearch`, `clickhouse`, `nats`. Anything
else is refused with `unknown_service_driver`; see
the [driver catalogue](/docs/reference/drivers).

## Wire the connection

A workload that declares `needs` receives `_URL` and its parts:
`_HOST`, `_PORT`, `_USER`, `_PASSWORD`, `_DATABASE`.

**Default variable names**

```yaml
workloads:
  web:
    role: Application
    image: ghcr.io/acme/shop:1.4.0
    needs:
      - {name: postgres, condition: healthy}
services:
  postgres: 17
```

**Your application**

```yaml
workloads:
  n8n:
    role: Application
    image: docker.n8n.io/n8nio/n8n:1.70.0
    needs:
      - name: postgres
        env:
          DB_POSTGRESDB_HOST:     host
          DB_POSTGRESDB_USER:     user
          DB_POSTGRESDB_PASSWORD: password
services:
  postgres: 16
```

The right-hand side is a connection part: `url`, `host`, `port`, `user`,
`password`, `database`. A part the driver does not have (a database on a cache)
is omitted rather than written empty.

`needs` also decides release order when `deployment.order` is absent.

## Three properties that always hold

- **A service outlives every release.** It runs in its own Compose project.
  Deploy can recreate PostgreSQL services with declared extensions when their
  configuration changes; otherwise, deploy and rollback leave services running.
  Neither removes their volumes.
- **Its credential follows the [driver credential contract](/docs/reference/drivers#what-a-declaration-gets-you).**
- **Its version binds into the release digest**, so a database upgrade under an
  untouched application cannot pass unnoticed.

## Reaching a pooler or a read replica

Connections own the credential, not the endpoint. Map only the parts you want and
author the host yourself:

```yaml
needs:
  - name: postgres
    env:
      DB_USER:     user
      DB_PASSWORD: password
env:
  DB_HOST: pgbouncer.internal
```

An application that reads a single connection URL cannot do this, because the URL
carries the credential and the credential never travels.

> **Shadowing a connection variable is refused**
>
> Claiming a name a managed-service connection supplies fails with
> `connection_variable_claimed`, naming both the variable and the service. The
> credential exists nowhere else, so nothing authored may claim its name. Validation prevents the
> collision before release ordering can affect it.

## When to use a daemon workload instead

| | `services` | `daemon` workload |
| --- | --- | --- |
| Image | Onebox picks it from driver + version | You name it |
| Credential | Generated on the server, once | Yours to supply |
| Release coupling | **Outlives every release** | Recreated on deploy |
| Connection wiring | Automatic via `needs` | You wire it |
| Available for | 11 drivers only | Anything containerised |

Use a daemon when you need something outside the driver set, or a topology the
driver does not provide.

> **MongoDB runs standalone, not a replica set**
>
> See the [MongoDB driver limitations](/docs/reference/drivers#known-limitations)
> for applications that need a replica set.

> **Back it up**
>
> A `postgres` service can declare `backup` and Onebox will keep a continuous
> off-host archive it can recover to a point in time; see
> [backups](/docs/guides/backups). Declaring the policy is not enough on its
> own: `ob backup enable` is what establishes it, and until then the service
> renders as an ordinary unprotected server.
>
> Every other driver declares `policy_qualified: false`, so a backup policy on
> it is refused, and copying its data off the box is
> your responsibility: a `job` workload on a `schedule` is the usual answer.

Full field list: [`services`](/docs/reference/fields/services).