castor-php / docker
Castor plugin for docker
Requires
- php: >=8.3
- castor-php/php-qa: ^0.3.0
Requires (Dev)
- jolicode/castor: ^1.1
- phpunit/phpunit: ^13.2
- twig/twig: ^3.28
This package is auto-updated.
Last update: 2026-07-24 15:52:53 UTC
README
A powerful Castor plugin that simplifies Docker-based development environments for PHP applications. This plugin automatically generates Docker Compose configurations and provides tasks to manage your infrastructure.
Features
- 🚀 Automatic Docker Compose configuration generation
- 🔧 Pre-configured services for common infrastructure components
- 🎯 Service-specific tasks for common operations
- 🔒 On-demand, locally-trusted HTTPS (with mkcert support)
- 🌐 Caddy-based reverse proxy with automatic routing from Docker labels
- 📦 Multi-stage Docker builds with registry caching
- 👥 Multi-application support in a single project
Installation
Add this plugin to your Castor project using Composer:
castor composer require castor-php/docker
Quick Start
- Create a
castor.phpfile in your project root:
<?php namespace project; use Castor\Attribute\AsContext; use Castor\Attribute\AsListener; use Castor\Context; use Castor\Docker\Event\RegisterServiceEvent; use Castor\Docker\Service\PostgresService; use Castor\Docker\Service\SymfonyService; #[AsContext(default: true)] function default_context(): Context { return new Context([ 'root_domain' => 'myproject.test', 'registry' => 'ghcr.io/mycompany/myproject' ]); } #[AsListener(RegisterServiceEvent::class)] function register_service(RegisterServiceEvent $event) { $postgresService = new PostgresService(); $event->addService($postgresService); $event->addService( (new SymfonyService(name: 'app', directory: __DIR__)) ->withDatabaseService($postgresService) ->addDomain('myproject.test') ->allowHttpAccess() ); }
- Run Castor to initialize your infrastructure:
castor docker:build castor docker:up
Available Services
SymfonyService
A comprehensive service for Symfony applications with FrankenPHP or PHP-FPM, Composer, and various development tools.
Configuration:
(new SymfonyService( name: 'app', // Service name directory: __DIR__, // Application directory version: '8.5', // PHP version mode: PhpMode::FrankenPhp, // PhpMode::FrankenPhp (default) or PhpMode::Fpm )) ->withDatabaseService($databaseService) ->addDomain('app.example.test') ->addDomain('example.test') ->allowHttpAccess() // Allow HTTP (default is HTTPS only) ->addWorker('messenger', 'php bin/console messenger:consume async') ->addExtension('redis') // Adds "php{version}-redis" (fpm) or the equivalent FrankenPHP extension ->withRedirectionIoKey('your-key') // Only applies in PhpMode::Fpm ->withFrankenPhpWorkerMode('public/index.php', num: 4) // Only applies in PhpMode::FrankenPhp
PHP Runtime Modes:
PhpMode::FrankenPhp(default) - Serves the app with FrankenPHP (dunglas/frankenphpimage + Caddy). Single process, no PHP-FPM/nginx.PhpMode::Fpm- Serves the app with the classic nginx + PHP-FPM stack.
Only the frontend (HTTP-serving) container differs between modes — the builder and worker containers are identical either way. Known limitations of PhpMode::FrankenPhp: withRedirectionIoKey() has no effect (nginx-only integration), and there is no /php-fpm-status monitoring endpoint.
Extensions:
By default the following PHP extensions are installed: apcu, bcmath, curl, iconv, intl, mbstring, pgsql, uuid, xml, zip. Add more with ->addExtension('name') — no custom Dockerfile needed anymore.
FrankenPHP Worker Mode:
->withFrankenPhpWorkerMode(string $script = 'public/index.php', ?int $num = null, bool $watch = true) boots $script once and keeps it in memory to handle every request (like Octane), instead of re-interpreting it per request. Only takes effect in PhpMode::FrankenPhp. Your application needs a compatible runtime to loop over incoming requests from that script — for Symfony, install runtime/frankenphp-symfony and point $script at public/index.php. $watch (enabled by default) restarts the worker automatically when files under the app directory change, which is what you want for local development.
Generated Tasks:
castor app:bash- Open a bash shell in the PHP containercastor app:install- Install Composer dependenciescastor app:composer- Run Composer commandscastor app:symfony- Run Symfony console commandscastor app:cache-clear- Clear application cachecastor app:cache-warmup- Warm up application cachecastor app:qa:phpstan- Run PHPStan static analysiscastor app:qa:cs- Run PHP CS Fixercastor app:qa:rector- Run Rector refactoringcastor app:qa:twig-cs- Fix Twig coding stylecastor app:db:migrate- Run database migrationscastor app:db:fixtures- Load database fixtures
Docker Services Created:
app- FrankenPHP or PHP-FPM frontend service (depending onmode)app-builder- Builder service for running commandsapp-worker-{name}- Worker services for background jobs
PostgresService
PostgreSQL database service.
Configuration:
new PostgresService()
Generated Tasks:
castor db:psql- Connect to PostgreSQL database
Docker Services Created:
postgres- PostgreSQL 16 server- Named volume:
postgres_data
Database URL: postgresql://app:app@postgres:5432/app?serverVersion=16&charset=utf8
MySQLService
MySQL database service.
Configuration:
new MySQLService( version: '8', // MySQL version rootPassword: 'root', // Root password database: 'app', // Database name )
Generated Tasks:
castor db:mysql- Connect to MySQL database
Docker Services Created:
mysql- MySQL server- Named volume:
mysql-data
Database URL: mysql://root:root@mysql:3306/app
MariaDBService
MariaDB database service.
Configuration:
new MariaDBService( version: '12.1', // MariaDB version rootPassword: 'root', // Root password database: 'app', // Database name )
Generated Tasks:
castor db:mariadb- Connect to MariaDB database
Docker Services Created:
mariadb- MariaDB server- Named volume:
mariadb-data
Database URL: mysql://root:root@mariadb:3306/app?serverVersion=mariadb-12.1&charset=utf8mb4
RedisService
Redis cache server with RedisInsight web UI.
Configuration:
new RedisService()
Docker Services Created:
redis- Redis 5 serverredis-insight- RedisInsight web UI (accessible via router)- Named volumes:
redis-data,redis-insight-data
Access: RedisInsight available at https://redis.{root_domain} when router is enabled
RabbitMQService
RabbitMQ message broker with management UI.
Configuration:
new RabbitMQService()
Docker Services Created:
rabbitmq- RabbitMQ server with management plugin- Named volume:
rabbitmq-data
Access: Management UI available at https://rabbitmq.{root_domain} when router is enabled
Default Credentials: guest:guest
ElasticsearchService
Elasticsearch search engine with Kibana.
Configuration:
new ElasticsearchService( version: '7.8.0', // Elasticsearch version )
Docker Services Created:
elasticsearch- Elasticsearch serverkibana- Kibana web UI- Named volume:
elasticsearch-data
Access:
- Elasticsearch:
https://elasticsearch.{root_domain}when router is enabled - Kibana:
https://kibana.{root_domain}when router is enabled
RedirectionioAgentService
Redirection.io agent for managing HTTP redirections.
Configuration:
new RedirectionioAgentService()
Docker Services Created:
redirectionio-agent- Redirection.io agent service
CaddyRouterService
Caddy reverse proxy (via caddy-docker-proxy) for routing HTTP/HTTPS traffic to services. Routes are built automatically from the caddy.* Docker labels emitted for each service that declares a domain — no static configuration file to maintain.
TLS certificates are minted on demand by Caddy's internal issuer the first time a domain is hit, so there is nothing to generate ahead of time. If the mkcert root CA is installed on your host, castor router:enable copies it into the router so those certificates are trusted by your browsers.
Configuration:
// Automatically registered, but can be customized new CaddyRouterService( sharedHomeDirectory: '.home', )
Generated Tasks:
castor router:enable- Enable the router service (and copy the mkcert CA if available)castor router:disable- Disable the router service
Docker Services Created:
router- Caddy reverse proxy- Named volume:
router-data(issued certificates and local CA) - Exposes ports: 80 (HTTP), 443 (HTTPS)
The router handles HTTP/HTTPS only — raw TCP protocols can't be hostname-routed.
Non-backend services (databases, Redis, RabbitMQ, Elasticsearch, ClickHouse,
Mailpit) therefore expose an opt-in <service>:expose task instead:
# Publish the service's port on the host (postgres → localhost:5432) castor postgres:expose # Pick a different host port (e.g. to avoid a clash with a local server) castor mysql:expose 3307 # Stop exposing it castor postgres:expose --stop
Each task runs a small socat forwarder container that publishes the port on
demand and forwards to the service over the project network — nothing is opened
on the host until you ask. The forwarders are tagged as part of the compose
project, so castor docker:destroy removes them along with everything else.
Under the hood the task calls the expose_service_port() helper, which your own
services can reuse.
Exposed services are remembered: castor docker:stop takes the forwarders down,
and the next castor docker:up brings them back automatically — you only expose
a service once, not on every restart (until you --stop it).
Docker Tasks
The plugin provides several Docker management tasks:
castor docker:build
Builds all Docker images.
Options:
--service- Build a specific service--profiles- Specify profiles to build
Example:
castor docker:build castor docker:build --service app castor docker:build --profiles builder
castor docker:up
Starts the infrastructure.
Options:
--service- Start a specific service--profiles- Specify profiles to start--build- Build images before starting
Example:
castor docker:up castor docker:up --build castor docker:up --service app
castor docker:stop
Stops running containers.
Options:
--service- Stop a specific service--profiles- Specify profiles to stop
Example:
castor docker:stop castor docker:stop --service app
castor docker:logs
Displays container logs.
Options:
--service- Show logs for a specific service--profiles- Specify profiles
Example:
castor docker:logs castor docker:logs --service app
castor docker:ps
Lists all containers with their status.
Example:
castor docker:ps
castor docker:destroy
Removes all containers, volumes, and networks.
Options:
--force/-f- Skip confirmation prompt
Example:
castor docker:destroy castor docker:destroy --force
⚠️ Warning: This permanently deletes all data including volumes!
castor docker:push
Pushes Docker images cache to the configured registry.
Options:
--dry-run- Show what would be pushed without actually pushing
Example:
castor docker:push castor docker:push --dry-run
Configuration
Context Variables
Configure your infrastructure in the default context:
#[AsContext(default: true)] function default_context(): Context { return new Context([ 'root_domain' => 'myproject.test', // Root domain for all services 'registry' => 'ghcr.io/org/project', // Docker registry for caching ]); }
Castor Variables
Additional variables can be set in your Castor configuration:
php_version- Default PHP version (default: from service config)registry- Docker registry for image cachingbuild_args- Additional build arguments for Docker builds
Generated Files
The plugin automatically generates and manages these files:
compose.yaml
Main Docker Compose file that includes:
compose.generated.yaml- Auto-generated service definitions (DO NOT EDIT)compose.override.yaml- Your local customizations
Example compose.yaml:
# This is your docker-compose file. It has been generated by Castor, but you can edit it if needed.
name: castor-docker-demo
include:
- path:
- compose.generated.yaml # This file is generated you should not remove or edit this line / file.
- compose.override.yaml # This file is for your local overrides of existing services.
# Here you can also add your own services.
compose.override.yaml
Use this file for local environment customizations:
# This file is for your local overrides. It is not generated by Castor.
services:
app:
environment:
- CUSTOM_ENV_VAR=value
Advanced Usage
Multiple Applications
You can run multiple applications in the same infrastructure:
#[AsListener(RegisterServiceEvent::class)] function register_service(RegisterServiceEvent $event) { $postgresService = new PostgresService(); $mysqlService = new MySQLService(); $event->addService($postgresService); $event->addService($mysqlService); $event->addService( (new SymfonyService(name: 'app1', directory: __DIR__ . '/app1')) ->withDatabaseService($postgresService) ->addDomain('app1.project.test') ->addDomain('project.test') ); $event->addService( (new SymfonyService(name: 'app2', directory: __DIR__ . '/app2', version: '8.2')) ->withDatabaseService($mysqlService) ->addDomain('app2.project.test') ); }
Custom Dockerfile
Extra PHP extensions don't need a custom Dockerfile anymore, use ->addExtension('name') instead (see SymfonyService).
For deeper customization, you can still provide a custom Dockerfile by extending the base PHP Dockerfile (or Dockerfile.frankenphp if you use PhpMode::FrankenPhp) and overriding any other block:
# syntax=ghcr.io/castor-php/twig-dockerfile:latest {% extends 'Dockerfile' %} {% block builder %} {{ parent() }} RUN echo "custom builder step" {% endblock %}
Then reference it in your service:
(new SymfonyService(name: 'app', directory: __DIR__)) ->withDockerfile(__DIR__ . '/Dockerfile')
Background Workers
Add background worker processes to your application:
(new SymfonyService(name: 'app', directory: __DIR__)) ->addWorker('messenger', 'php bin/console messenger:consume async --time-limit=3600') ->addWorker('notifications', 'php bin/console app:process-notifications')
Each worker runs as a separate Docker container.
Quality Assurance Tools
The plugin integrates with common PHP QA tools:
# Run PHPStan castor app:qa:phpstan # Fix code style castor app:qa:cs # Run Rector castor app:qa:rector # Fix Twig templates castor app:qa:twig-cs
Configure versions in your service:
(new SymfonyService(name: 'app', directory: __DIR__)) ->addPhpStanExtraDependency('phpstan/phpstan-symfony', '^2.0')
Docker Profiles
Services can be organized into profiles:
default- Services that start by defaultrouter- Caddy reverse proxybuilder- Build and CI/CD services
Control which profiles to use:
castor docker:up --profiles default,router castor docker:build --profiles builder
SSL Certificates
The Caddy router provisions TLS certificates on demand: the first time a domain is requested, Caddy's internal issuer mints a certificate for it. There is no certificate to generate or renew manually.
With mkcert (Recommended)
To make those certificates trusted by your browsers (no security warning):
- Install mkcert: https://github.com/FiloSottile/mkcert
- Install the CA in your system trust store:
mkcert -install - (Re)start the router:
castor router:enable
router:enable copies the mkcert root CA into the router, which then signs the on-demand certificates with it.
Without mkcert
If mkcert is not available, Caddy falls back to its own local CA. HTTPS still
works, but you'll need to accept the security warning in your browser (or add
Caddy's root CA — stored in the router-data volume — to your trust store).
Environment Variables
The plugin automatically sets these environment variables for your services:
PHP_VERSION- PHP version being usedPROJECT_NAME- Docker Compose project namePROJECT_ROOT_DOMAIN- Root domain for the projectREGISTRY- Docker registry URLDATABASE_URL- Database connection string (when using a database service)
Troubleshooting
Port Conflicts
If ports 80 or 443 are already in use:
- Stop the conflicting services
- Or modify the router port mappings in your
compose.override.yaml
Permission Issues
The plugin runs containers with your user ID to avoid permission issues. If you encounter problems:
- Check that the
.homedirectory is writable - Verify volume mount permissions
Containers Won't Start
- Check logs:
castor docker:logs - Verify builds:
castor docker:build - Check Docker daemon:
docker ps
Router Not Working
- Enable the router:
castor router:enable - For locally-trusted certificates, install mkcert (
mkcert -install) and re-runcastor router:enable - Check that domains resolve to
127.0.0.1(add to/etc/hostsif needed)
Examples
See the example directory for a complete working example with multiple applications and services.
License
This plugin is part of the Castor project.