Skip to content

Databases

Every database service implements DatabaseServiceInterface, so it can be handed to an application with ->withDatabaseService(). The application then gets a DATABASE_URL environment variable and waits for the database to be healthy before starting:

$postgres = new PostgresService();
$event->addService($postgres);

$event->addService(
    (new SymfonyService('app'))->withDirectory(__DIR__)->withDatabaseService($postgres)
);

Each of them also exposes a {name}:expose task to reach the server from the host with a native client — see tasks.

PostgresService

(new PostgresService())
    ->withVersion('16')             // PostgreSQL version (default: 16)
  • Task: castor postgres:client — a psql session
  • Containers: postgres, named volume postgres_data
  • Database URL: postgresql://app:app@postgres:5432/app?serverVersion=16&charset=utf8

MySQLService

(new MySQLService())
    ->withVersion('8')              // MySQL version (default: 8)
    ->withRootPassword('root')      // Root password (default: root)
    ->withDatabase('app')           // Database name (default: app)
  • Configuration: withSetting() and friends
  • Task: castor mysql:client — a mysql session
  • Containers: mysql, named volume mysql-data
  • Database URL: mysql://root:root@mysql:3306/app

MariaDBService

(new MariaDBService())
    ->withVersion('12.1')           // MariaDB version (default: 12.1)
    ->withRootPassword('root')      // Root password (default: root)
    ->withDatabase('app')           // Database name (default: app)
  • Configuration: withSetting() and friends
  • Task: castor mariadb:client — a mariadb session
  • Containers: mariadb, named volume mariadb-data
  • Database URL: mysql://root:root@mariadb:3306/app?serverVersion=mariadb-12.1&charset=utf8mb4

ClickhouseService

(new ClickhouseService())
    ->withVersion('25.8')           // ClickHouse version (default: 25.8)
    ->withDatabase('app')           // Database name (default: app)
    ->withCredentials('app', 'app') // User and password (default: app / app)
    ->withBackup()                  // Install Altinity clickhouse-backup in the image
  • Task: castor clickhouse:client — a clickhouse-client session
  • Containers: clickhouse and clickhouse-keeper, named volume clickhouse-data
  • UI: https://clickhouse.{root_domain} when the router is enabled

Note

ClickHouse is not a DatabaseServiceInterface: it is meant to sit next to your main database rather than to back DATABASE_URL.

Configuring the MySQL and MariaDB servers

Both read every *.cnf of /etc/mysql/conf.d on top of their built-in defaults, and the plugin ships one file there:

(new MySQLService())
    ->withSetting('max_connections', 500)
    ->withSetting('innodb_buffer_pool_size', '1G')
    ->withSetting('slow_query_log', true)          // ON / OFF for a boolean
    ->withSetting('skip-name-resolve')             // a flag that takes no value
    ->withSettings(['sort_buffer_size' => '4M'])   // several at once

which generates:

# This file is generated by Castor. Do not edit it manually.
[mysqld]
max_connections = 500
innodb_buffer_pool_size = 1G
slow_query_log = ON
skip-name-resolve
sort_buffer_size = 4M

For what withSetting() cannot express — another section, a comment you want to keep — append a raw block, or a file of your project:

->withConfiguration("[client]\ndefault-character-set = utf8mb4")
->withConfigurationFile(__DIR__ . '/docker/my.cnf')

All three merge into the same file, settings first, in the order you declared them.

The result is shipped as a compose config, not as a bind mount: the content ends up in compose.generated.yaml, so there is no host directory for Docker to create as root, and no file whose permissions the server might refuse — MySQL ignores a world-writable .cnf.

The server reads it once, at boot, and compose does not recreate a container when only the content of a config changed. The plugin stamps a digest of it in a castor.config.{name}-config label, so castor docker:up picks a change up on its own — no --force-recreate.

withConfigurationFile() reads the file when the compose file is generated, and raises if the path does not exist. A mistyped path is reported there and then, rather than leaving the server silently unconfigured. It also means editing that file takes effect on the next Castor run, followed by a restart of the container.

Several instances of the same database

withName() overrides the name a service gives itself, which is what lets you register it twice:

$main = new PostgresService();
$analytics = (new PostgresService())->withName('analytics');

$event->addService($main);
$event->addService($analytics);

$event->addService((new SymfonyService('app'))->withDatabaseService($analytics));

The container, the named volume (analytics_data), the connection string (postgresql://app:app@analytics:5432/app) and the expose task (castor analytics:expose) all follow the name.

Every task of a service is named {service}:{task}, so the two instances get two full task sets rather than fighting over one: castor postgres:client and castor analytics:client, castor postgres:expose and castor analytics:expose.