Documentation

Databases

Provision managed PostgreSQL, MySQL, MongoDB, and Redis services on your server with protected default resource limits and simple connection workflows.

Supported databases

nouva provides one click PostgreSQL, MySQL, MongoDB, and Redis services. Pick a version, reserve storage if needed, and change the protected CPU and memory limits only when a database needs different ones.

  • PostgreSQL 18, 17, 16, and 15. Default: 17.
  • MySQL 8.4, and 8.0. Default: 8.4.
  • MongoDB 8.0, and 7.0. Default: 8.0.
  • Redis 7.4, 7.2, and 7.0. Default: 7.4.
  • MongoDB 8.0 refuses to start on some Linux kernels, including the Ubuntu 7.0 series. On a server whose kernel it rejects the version picker marks it unavailable and preselects MongoDB 7.0. The server's Health tab lists which database versions can run on it.
  • Every database starts with protected CPU, memory, and PID limits and no swap. PostgreSQL, MySQL, and MongoDB: 0.5 vCPU • 1 GB RAM • 512 PIDs. Redis: 0.25 vCPU • 256 MB RAM • 256 PIDs. Set custom CPU and memory limits, or allow swap, per database. The PID limit stays managed by the platform.
  • Reserve storage separately from compute limits.

Connection details

nouva injects connection credentials into your services as environment variables. Use the private URLs for services inside the same project, and public URLs for external clients.

  • Postgres: DATABASE_URL, DATABASE_PRIVATE_URL, DATABASE_PUBLIC_URL.
  • Postgres also exposes DATABASE_SESSION_URL for connections that need one PostgreSQL session. See connection pooling below.
  • Postgres URLs include ?sslmode=require (TLS required).
  • nouva also sets PGSSLMODE=require for convenience in app configs.
  • MySQL: MYSQL_URL, MYSQL_PRIVATE_URL, MYSQL_PUBLIC_URL.
  • MySQL also exposes MYSQLHOST, MYSQLPORT, MYSQLUSER, MYSQLPASSWORD, and MYSQLDATABASE.
  • MongoDB: MONGODB_URI, MONGODB_PRIVATE_URI, MONGODB_PUBLIC_URI.
  • MongoDB also exposes MONGODB_HOST, MONGODB_PORT, and MONGODB_AUTH_SOURCE.
  • Redis: REDIS_URL, REDIS_PRIVATE_URL, REDIS_PUBLIC_URL.

Connection pooling

Managed PostgreSQL serves two endpoints on the same private host, both over TLS. The URLs above reach PgBouncer on port 5432. DATABASE_SESSION_URL reaches PostgreSQL itself on port 5433 and stays on the project network — enabling public access publishes the pooled port only.

  • DATABASE_URL: Shares a small set of PostgreSQL backends across many clients, one transaction at a time. The default for application traffic.
  • DATABASE_SESSION_URL: Bypasses the pooler and keeps one PostgreSQL session for the life of the connection. Required for session-scoped features, and it consumes a real backend, so pool it in your application and keep the connection count small.

Transaction pooling gives each transaction whichever server connection is free, so state that belongs to a PostgreSQL session rather than to a transaction can end up on a backend your client no longer holds. Move that work to DATABASE_SESSION_URL:

  • Session-level advisory locks (pg_advisory_lock) — The lock can be acquired on one backend and released on another, so it leaks onto an idle backend and blocks later work. Transaction-level pg_advisory_xact_lock is safe.
  • LISTEN / NOTIFY — The listening backend goes back to the pool when the transaction ends, so notifications are delivered to whichever client happens to hold that backend, or to nobody.
  • Session-wide SET and RESET — A setting applied outside a transaction does not follow later queries. SET LOCAL inside a transaction is safe.
  • SQL-level PREPARE / EXECUTE / DEALLOCATE — PgBouncer tracks protocol-level prepared statements, not SQL statements, so a name prepared on one backend is missing on the next. This is what breaks PHP PDO with native prepares, which deallocates by SQL statement.
  • WITH HOLD cursors and other cursors used after COMMIT — The cursor is left on a backend the client no longer owns. Cursors opened and consumed inside one transaction are safe.
  • Temporary tables — The table belongs to the session that created it, so later statements can land on a backend that has never seen it.

Everything else keeps using the pooled URL. A session connection holds a real PostgreSQL backend for as long as it is open, so pool it in your application and keep the connection count small — the database's max_connections bounds it, not the pooler.

PHP, PDO, and prepared statements

The pooler tracks up to 200protocol-level prepared statements per client, so most drivers use the pooled URL unchanged. PHP's PDO before 8.4 is the exception. It releases a prepared statement by sending DEALLOCATE pdo_stmt_… as a SQL statement, and the pooler renames protocol-level statements on the server, so that name is never found. The failed DEALLOCATE aborts the surrounding transaction and every later statement in it fails with SQLSTATE 25P02.

  • Set PDO::ATTR_EMULATE_PREPARES => true on the connection. Keep the provided URL, its TLS mode, and your bound parameters exactly as they are — PDO builds the statement client side instead of preparing it on the server.
  • Or run PHP 8.4 or newer built against libpq 17 or newer, which closes statements over the protocol instead of by SQL. Both halves are required: PHP 8.3 fails on this path even when it is linked against libpq 17.
  • Or move the connection to DATABASE_SESSION_URL, which never reassigns the session.

Connecting and running SELECT 1 succeeds in both the broken and the working setup, so verify with a parameterized statement inside a transaction followed by a read back.

Backups and recovery

PostgreSQL, MySQL, and Redis support backups and restores today. MongoDB supports backups, but MongoDB restores and PITR are not available yet. Restores stage a replacement volume on the existing service, and applying the staged change redeploys that same service with the restored data.

Importing an external backup

PostgreSQL and Redis services accept a backup you took somewhere else. Register the artifact's SHA-256 digest and size, upload it, then start the import. You never expose a database port or paste a connection string into a command, and nothing you upload is restored over data that is still serving traffic.

In the dashboard, use Import a backup on the database's Backups tab. The customer API offers the same steps under /v1/services/:serviceId/database-imports.

  • PostgreSQL: a custom-format archive from pg_dump -Fc. Plain SQL dumps and directory or tar archives are not accepted.
  • Redis: a native RDB snapshot, as written by SAVE, BGSAVE, or the file already at dump.rdb. AOF files are not accepted.
  • Artifacts up to 5 GB. The source engine version cannot be newer than the destination.

Before anything is restored, nouva verifies that the bytes hash to the digest you registered, that the artifact is the format you declared by reading its header rather than its name, and that the engine version recorded in that header can be replayed into the destination. An artifact that cannot be read is refused; it is never restored with a warning.

The restore runs into a new staged volume that your service is not using, and the staged copy is validated by starting the destination engine against it. Your existing volume stays attached and untouched until you apply the import, and a failed import leaves the service exactly as it was. Every import produces a receipt recording the digest, the format, the source and destination versions, and what was found in the staged copy - relation counts for PostgreSQL, key and expiring-key counts for Redis.

What is not checked is the meaning of what you uploaded. A dump is your own SQL or your own keys; nouva verifies where it came from and that it loads, not that its contents are what you intended. Import into a non-production service first.

Best practices

  • Keep migrations in your app repository and deploy them alongside code changes.
  • Use private URLs for services running inside nouva for lower latency.
  • Practice the restore workflow on non-production databases before you need it.