# Your own database, Redis and storage

Use a PostgreSQL, Redis and S3 bucket you already run, instead of the built-in ones.

For each store, the setup asks: **built in**, a container on this server, or **your own**. [Several servers](https://docs.gorunner.io/self-host/several-servers) need all three of your own.

| Store | Built in | Your own |
| --- | --- | --- |
| Database | PostgreSQL, in the `pgdata` volume | PostgreSQL 15 or newer: RDS, Cloud SQL, Azure, your own |
| Redis | Redis, set up as GoRunner needs | Redis 6.2 or newer: ElastiCache, Upstash, Memorystore |
| File storage | SeaweedFS | Any S3 bucket: AWS, R2, MinIO, Backblaze, Wasabi. Or none: step data stays in the database. |

`.env` names each choice in a word: `GORUNNER_POSTGRES` and `GORUNNER_REDIS` are `bundled` or `external`; `GORUNNER_STORAGE` also takes `none`. A deleted line stops GoRunner, rather than starting an empty built-in store.

## Give the address

As separate fields, a password needs no escaping; as one address (`DATABASE_URL`, `REDIS_URL`), it does, and wins when both are set. The host is a name or an address alone, never with its port.

`.env`

```
GORUNNER_POSTGRES=external
POSTGRES_HOST=gorunner-db.c9x2.eu-west-1.rds.amazonaws.com
POSTGRES_PORT=5432
POSTGRES_DB=gorunner
POSTGRES_USER=gorunner
POSTGRES_PASSWORD=<password>
POSTGRES_CA_FILE=/certs/rds.pem
```

## Your own PostgreSQL

- **Version 15 or newer**: GoRunner’s tables use it.
- **A user that owns GoRunner’s tables**: And can create tables in `public`. If another user owns them, the check prints the SQL that hands them over.
- `uuid-ossp`, `citext`: Added at the first start. The user needs `CREATE` on the database, or add them yourself.
- `pgcrypto`: Needed at the first start only: GoRunner adds it, then removes it because nothing uses it. If you or your provider added it, it may stay, unused. An update never needs it.
- **Enough connections**: The check works out each process’s pool, `DB_MAX_CONNS`, and keeps 5 free for backups and an admin (`DB_ADMIN_HEADROOM`).
- **No pooler in transaction mode**: PgBouncer in transaction mode can freeze updates; the check refuses it. Session mode works.

**TLS.** `POSTGRES_SSLMODE` is `require` by default, or `verify-full` with your provider’s CA file in `certs/`, named as `/certs/rds.pem`. With `DATABASE_URL`, add `sslrootcert=/certs/rds.pem&sslmode=verify-full`. A file it can’t use stops it. See [The database CA file](https://docs.gorunner.io/self-host/troubleshooting#database-ca-file).

**How do I add the extensions myself?**

For a new database. One that has started once needs only `uuid-ossp` and `citext`. As the database’s owner or your provider’s admin user, connected to GoRunner’s database, once:

```sql
GRANT CREATE ON DATABASE gorunner TO gorunner;
-- or add them yourself:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS citext;
```

## Your own Redis

- **Version 6.2 or newer**: The check refuses older ones.
- `maxmemory-policy noeviction`: Redis must never delete keys to make room. Managed services often default to `volatile-lru`: change it in the parameter group.
- **Room under `maxmemory`**: At the limit, Redis refuses new writes: new runs wait instead of starting, schedules don’t fire and sign-in fails until memory is freed. [Health](https://docs.gorunner.io/self-host/health#services) warns from 70 %.
- **Saved to disk**: AOF or snapshots. Without it, a restart signs everyone out. The check warns.
- **Not in cluster mode**: The check refuses it. ElastiCache Serverless and MemoryDB only run in cluster mode.
- **A database of its own**: `REDIS_DB`, `0` by default.

For TLS, set `REDIS_TLS=true` or use a `rediss://` address. For a user as well as a password, set `REDIS_USER`.

### What Redis holds

Wake-ups for the workers, which worker runs which run, what each run in progress has finished, sessions and short-lived codes. Runs and the Storage and Queue steps’ data are in PostgreSQL. Losing Redis signs everyone out, and a run in progress that could change something outside fails with **Progress unknown**. No run is lost.

**Using Amazon ElastiCache?**

- **Cluster mode disabled**: One primary, replicas if you like. GoRunner uses the primary endpoint.
- **A parameter group**: Your own, with `maxmemory-policy` set to `noeviction`.
- **Encryption in transit and AUTH**: `REDIS_TLS=true`; the token as `REDIS_PASSWORD`, with `REDIS_USER` for role-based access.
- **Snapshots**: Kept a day or more. There is no AOF on current engines.

ElastiCache doesn’t allow `CONFIG GET`, so the check may warn it couldn’t confirm the policy. That is expected: check the parameter group yourself.

## Your own S3 bucket

- `S3_ENDPOINT`: Such as `https://s3.eu-central-1.amazonaws.com`, or your MinIO or R2 address.
- `S3_BUCKET`, `S3_REGION`: `gorunner` by default; `auto` for R2. GoRunner creates the bucket if the key may.
- `S3_ACCESS_KEY`, `S3_SECRET_KEY`: A key that can write, read and delete objects. The check tries all three.
- `S3_FORCE_PATH_STYLE`: `true` for MinIO and SeaweedFS.

The setup keeps step data up to 4 KB in the database (`STORAGE_INLINE_THRESHOLD_KB`). With **none**, all of it stays there; turning storage off later is refused while step data still lives in storage.

**Use the server’s AWS role instead of a key?**

Leave both keys empty. The containers read the role from the instance metadata service, one network hop further than the server, so set the hop limit to 2:

```bash
aws ec2 modify-instance-metadata-options --instance-id <id> \
  --http-put-response-hop-limit 2 --http-tokens required
```

Workflow steps never reach the metadata service, whatever the hop limit. A bucket that keeps versions needs `s3:DeleteObjectVersion` too, and a lifecycle rule for old versions.

## Certificates

Every container trusts the `.pem` files in the install’s `certs/` folder, beside the public authorities. On Kubernetes, mount them at `/certs` ([Helm values](https://docs.gorunner.io/self-host/helm-values#certificates-and-extra-volumes)).

## Changing later

Run `./setup.sh` again and change the answer. A moved store or a changed address is listed, and you type `change`. The other store doesn’t hold your data: **move the data first**, with your database’s dump and restore. The built-in stores’ passwords stay in `.env.previous-stores`.

Changed a store word in `.env` by hand? The update then needs `--confirm-store-change`.

## Related

- [Back up and restore](https://docs.gorunner.io/self-host/backups): Including your own stores · 10 min
- [The preflight check](https://docs.gorunner.io/self-host/preflight-check): What it tests in each store
- [Several servers](https://docs.gorunner.io/self-host/several-servers): More workers on more servers · 15 min

Source: https://docs.gorunner.io/self-host/own-stores
