Multiple applications¶
Nothing stops you from registering several applications in the same infrastructure — each with its own directory, PHP version, database and domains:
#[AsListener(RegisterServiceEvent::class)]
function register_service(RegisterServiceEvent $event)
{
$postgresService = new PostgresService();
$mysqlService = new MySQLService();
$event->addService($postgresService);
$event->addService($mysqlService);
$event->addService(
(new SymfonyService('app1'))
->withDirectory(__DIR__ . '/app1')
->withDatabaseService($postgresService)
->withDomain('app1.project.test', 'project.test')
);
$event->addService(
(new SymfonyService('app2'))
->withDirectory(__DIR__ . '/app2')
->withVersion('8.2')
->withDatabaseService($mysqlService)
->withDomain('app2.project.test')
);
}
Each application gets its own set of tasks, prefixed with its name:
castor app1:install, castor app2:symfony, and so on.
Languages can be mixed the same way — a PHP front-end, a Go API and a Rust worker in the same stack, all routed by the same router:
$event->addService((new GoService('api'))->withDirectory(__DIR__ . '/api')->withDomain('api.project.test'));
$event->addService((new RustService('crawler'))->withDirectory(__DIR__ . '/crawler'));
Monorepos¶
The shape above gives each application its own directory, its own image and its own builder container. That stops paying off once a single Git root holds a dozen of them: the toolchain gets declared over and over, and a container can only see its own sub-directory.
Three things change.
Mount the root, name the sub-directory¶
withDirectory() is what gets mounted, withWorkingDirectory() is where the
commands run inside it. Mount the repository and every container can read what
the others produce:
(new SymfonyService('backend'))
->withDirectory(__DIR__) // the repository root
->withWorkingDirectory('apps/backend') // where composer and the console run
One builder per language, not per application¶
PHP applications share a builder container with
withSharedBuilder().
Compiled languages go further and split the compiler from the runtime:
RustBuilder or
GoBuilder holds the toolchain and declares the
applications it compiles, and one
BinaryRunService per binary runs the
result.
$rust = (new RustBuilder('rust-builder'))
->withDirectory(__DIR__)
->addRustupTarget('x86_64-unknown-linux-musl')
->withApp('agent/agent-application', target: 'x86_64-unknown-linux-musl')
->withApp('server/log-injector');
$event->addService($rust);
$event->addService(
(new BinaryRunService('agent', 'agent/target/x86_64-unknown-linux-musl/debug/agent-application'))
->withBuilder($rust, 'agent-application')
->withDomain('agent.project.test')
);
One image, one registry cache reference, one place where the toolchain is
declared — and castor agent-application:build runs in the builder rather than
inside a running binary.
Call the other applications by their real domains¶
https://backend.project.test works from inside a container, not just from your
browser: every container gets an extra_hosts entry for each domain of the
project. Nothing to configure, and the URL is the same in development and in
production — see router and
HTTPS.
A complete example¶
The example/ directory
of the repository is exactly that: one root with two PHP applications sharing a
builder, a Rust binary and a Go binary each built by their language's builder,
and a container calling another through its public domain.