Skip to content

Configuring services

Services take only their identity in the constructor — a name for the application services, nothing at all for the others. Everything else is set with fluent with*() methods, so you only write the options you actually change:

(new RustService('api'))
    ->withVersion('1.90')
    ->withDirectory(__DIR__ . '/api')
    ->withDomain('api.project.test')

Behaviour traits

Those methods come from a small set of traits in Castor\Docker\Service\Behaviour, shared by every service that needs the behaviour. Use them in your own services to get the same API for free.

Trait Methods Used by
HasVersion withVersion(), getVersion() every versioned service
HasDomains withDomain(...$domains), getDomains() every routed service
HasHttpAccess withHttpAccess(), isHttpAccessAllowed() every routed service
HasHttpRouting the two above + withPort(), getPort(), applyHttpRouting() every routed service
HasDirectory withDirectory(), getDirectory(), withWorkingDirectory(), getWorkingDirectory() every service mounting sources
HasSharedHomeDirectory withSharedHomeDirectory(), getSharedHomeDirectory() PHPService, GoService, RustService, the builders
HasDockerfile withDockerfile(), getDockerfile() PHPService, GoService, RustService, the builders
HasEnvironment withEnvironment(), getEnvironment(), applyEnvironment() BinaryRunService
HasName withName(), getName() every service naming itself
HasMysqlConfiguration withSetting(), withSettings(), withConfiguration(), withConfigurationFile() MySQLService, MariaDBService

Registering the same service twice

Application services take their name in the constructor. The infrastructure ones name themselves, and withName() overrides that default — which is what makes a second instance possible:

$event->addService(new PostgresService());
$event->addService((new PostgresService())->withName('analytics'));

Everything the service generates is derived from the name: the compose service, the named volumes (analytics_data), the routed domain (analytics.project.test), the connection string (postgresql://…@analytics:5432/…) and the companion containers — redis-insight becomes sessions-insight, clickhouse-keeper becomes events-keeper.

Its tasks too: they are named {service}:{task}, so two instances give you two full task sets rather than a collision — castor postgres:client and castor analytics:client, castor postgres:expose and castor analytics:expose.

The task half names what it does, not what it runs: the session a database opens is client on all of them, rather than psql on one and mysql on the next.

One generated name is an exception, because it is not derived from the service one: the Kibana container stays kibana for the first instance, and a renamed Elasticsearch gets logs-kibana.

Names are not checked for collisions: register two services under the same name and the second silently merges into the first, since ComposeBuilder::service() returns the existing builder.

Mount, working directory, binary

HasDirectory carries two distinct paths, because in a monorepo they are two distinct things: withDirectory() is the host directory mounted in the container, and withWorkingDirectory() is where the commands run below it — relative to the mount, . by default. GoService, RustService and BinaryRunService add a third with withBinaryPath(), for the binary the container starts.

They only coincide when an application owns its own directory, which is why the defaults leave the generated file exactly as it was.

HasVersion and HasDockerfile require the service to declare its own fallback with getDefaultVersion() / getDefaultDockerfile(), and HasHttpRouting lets a service override getDefaultPort() — that is how RustService defaults to 8080 while everything else defaults to 80.

Lazy defaults

Defaults are resolved when the compose file is generated, not in the constructor. PHPService picks its Dockerfile from the runtime mode, so this works whatever the call order is:

(new SymfonyService('app'))
    ->withDirectory(__DIR__)
    ->withMode(PhpMode::Fpm) // still selects the FPM Dockerfile

Domains and HTTP access

withDomain() is variadic and de-duplicates, so these are equivalent:

->withDomain('app.test', 'www.app.test')
->withDomain('app.test')->withDomain('www.app.test')

withPort() names the port the service listens on inside the container, and the routing labels always carry it — routing without one would let the router pick whichever port the image exposes, and answer 502 when it guesses wrong. Services that listen somewhere other than their default say so:

(new RustService('api'))->withPort(18089)->withDomain('api.project.test')

A service with at least one domain gets the caddy.* labels the router turns into routes. HTTPS is served with an on-demand, locally-trusted certificate, and plain HTTP redirects to it. Call ->withHttpAccess() to also serve plain HTTP without the redirect.