Repository: jolicode/docker-starter Branch: main Commit: cb03fe0c3427 Files: 45 Total size: 94.2 KB Directory structure: gitextract_is9p0cpd/ ├── .castor/ │ ├── context.php │ ├── database.php │ ├── docker.php │ ├── init.php │ └── qa.php ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── cache.yml │ └── ci.yml ├── .gitignore ├── .home/ │ └── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.dist.md ├── README.md ├── application/ │ └── public/ │ └── index.php ├── castor.php ├── infrastructure/ │ └── docker/ │ ├── docker-compose.dev.yml │ ├── docker-compose.yml │ └── services/ │ ├── php/ │ │ ├── Dockerfile │ │ ├── base/ │ │ │ ├── php-configuration/ │ │ │ │ └── mods-available/ │ │ │ │ └── app-default.ini │ │ │ └── sudo.sh │ │ ├── builder/ │ │ │ └── php-configuration/ │ │ │ └── mods-available/ │ │ │ └── app-builder.ini │ │ └── frontend/ │ │ ├── etc/ │ │ │ ├── nginx/ │ │ │ │ ├── environments │ │ │ │ └── nginx.conf │ │ │ └── service/ │ │ │ ├── nginx/ │ │ │ │ ├── run │ │ │ │ └── supervise/ │ │ │ │ └── .gitignore │ │ │ └── php-fpm/ │ │ │ ├── run │ │ │ └── supervise/ │ │ │ └── .gitignore │ │ └── php-configuration/ │ │ ├── fpm/ │ │ │ └── php-fpm.conf │ │ └── mods-available/ │ │ └── app-fpm.ini │ └── router/ │ ├── Dockerfile │ ├── certs/ │ │ └── .gitkeep │ ├── generate-ssl.sh │ ├── openssl.cnf │ └── traefik/ │ ├── dynamic_conf.yaml │ └── traefik.yaml ├── phpstan.neon └── tools/ ├── php-cs-fixer/ │ ├── .gitignore │ └── composer.json ├── phpstan/ │ ├── .gitignore │ └── composer.json └── twig-cs-fixer/ ├── .gitignore └── composer.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .castor/context.php ================================================ 'app', 'root_domain' => 'app.test', 'extra_domains' => [], 'php_version' => '8.5', 'docker_compose_files' => [ 'docker-compose.yml', 'docker-compose.dev.yml', ], 'docker_compose_run_environment' => [], 'macos' => false, 'power_shell' => false, // check if posix_geteuid is available, if not, use getmyuid (windows) 'user_id' => \function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(), 'root_dir' => \dirname(__DIR__), ]; if (file_exists($data['root_dir'] . '/infrastructure/docker/docker-compose.override.yml')) { $data['docker_compose_files'][] = 'docker-compose.override.yml'; } $platform = strtolower(php_uname('s')); if (str_contains($platform, 'darwin')) { $data['macos'] = true; } elseif (\in_array($platform, ['win32', 'win64', 'windows nt'])) { $data['power_shell'] = true; } // 2³² - 1 if (false === $data['user_id'] || $data['user_id'] > 4294967295) { $data['user_id'] = 1000; } if (0 === $data['user_id']) { log('Running as root? Fallback to fake user id.', 'warning'); $data['user_id'] = 1000; } return new Context( $data, pty: Process::isPtySupported(), environment: [ 'BUILDKIT_PROGRESS' => 'plain', ] ); } #[AsContext(name: 'test')] function create_test_context(): Context { $c = create_default_context(); return $c ->withEnvironment([ 'APP_ENV' => 'test', ]) ; } #[AsContext(name: 'ci')] function create_ci_context(): Context { $c = create_test_context(); return $c ->withData( // override the default context here [ 'docker_compose_files' => [ 'docker-compose.yml', // Usually, the following service is not be needed in the CI 'docker-compose.dev.yml', // 'docker-compose.ci.yml', ], ], recursive: false ) ->withEnvironment([ 'COMPOSE_ANSI' => 'never', ]) ; } ================================================ FILE: .castor/database.php ================================================ title('Connecting to the PostgreSQL database'); docker_compose(['exec', 'postgres', 'psql', '-U', 'app', 'app'], context()->toInteractive()); } ================================================ FILE: .castor/docker.php ================================================ title('About this project'); io()->comment('Run castor to display all available commands.'); io()->comment('Run castor about to display this project help.'); io()->comment('Run castor help [command] to display Castor help.'); io()->section('Available URLs for this project:'); $urls = [variable('root_domain'), ...variable('extra_domains')]; try { $routers = http_client() ->request('GET', \sprintf('http://%s:8080/api/http/routers', variable('root_domain'))) ->toArray() ; $projectName = variable('project_name'); foreach ($routers as $router) { if (!preg_match("{^{$projectName}-(.*)@docker$}", $router['name'])) { continue; } if ("frontend-{$projectName}" === $router['service']) { continue; } if (!preg_match('{^Host\(`(?P.*)`\)$}', $router['rule'], $matches)) { continue; } $hosts = explode('`) || Host(`', $matches['hosts']); $urls = [...$urls, ...$hosts]; } } catch (HttpExceptionInterface) { } io()->listing(array_map(fn ($url) => "https://{$url}", array_unique($urls))); } #[AsTask(description: 'Opens the project in your browser', namespace: '', aliases: ['open'])] function open_project(): void { open('https://' . variable('root_domain')); } #[AsTask(description: 'Builds the infrastructure', aliases: ['build'])] function build( #[AsOption(description: 'The service to build (default: all services)', autocomplete: 'docker\get_service_names')] ?string $service = null, ?string $profile = null, ): void { generate_certificates(force: false); io()->title('Building infrastructure'); $command = []; $command[] = '--profile'; if ($profile) { $command[] = $profile; } else { $command[] = '*'; } $command = [ ...$command, 'build', '--build-arg', 'PHP_VERSION=' . variable('php_version'), '--build-arg', 'PROJECT_NAME=' . variable('project_name'), ]; if ($service) { $command[] = $service; } docker_compose($command); } /** * @param list $profiles */ #[AsTask(description: 'Builds and starts the infrastructure', aliases: ['up'])] function up( #[AsOption(description: 'The service to start (default: all services)', autocomplete: 'docker\get_service_names')] ?string $service = null, #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] array $profiles = [], ): void { if (!$service && !$profiles) { io()->title('Starting infrastructure'); } $command = ['up', '--detach', '--wait', '--no-build']; if ($service) { $command[] = $service; } try { docker_compose($command, profiles: $profiles); } catch (ExceptionInterface $e) { io()->error('An error occurred while starting the infrastructure.'); io()->note('Did you forget to run "castor docker:build"?'); io()->note('Or you forget to login to the registry?'); throw $e; } } /** * @param list $profiles */ #[AsTask(description: 'Stops the infrastructure', aliases: ['stop'])] function stop( #[AsOption(description: 'The service to stop (default: all services)', autocomplete: 'docker\get_service_names')] ?string $service = null, #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] array $profiles = [], ): void { if (!$service || !$profiles) { io()->title('Stopping infrastructure'); } $command = ['stop']; if ($service) { $command[] = $service; } docker_compose($command, profiles: $profiles); } /** * @param array $params */ #[AsTask(description: 'Opens a shell (bash) or proxy any command to the builder container', aliases: ['builder'])] function builder(#[AsRawTokens] array $params = []): int { if (0 === \count($params)) { $params = ['bash']; } $c = context() ->toInteractive() ->withEnvironment($_ENV + $_SERVER) ; return (int) docker_compose_run(implode(' ', $params), c: $c)->getExitCode(); } /** * @param list $profiles */ #[AsTask(description: 'Displays infrastructure logs', aliases: ['logs'])] function logs( ?string $service = null, #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] array $profiles = [], ): void { $command = ['logs', '-f', '--tail', '150']; if ($service) { $command[] = $service; } docker_compose($command, c: context()->withTty(), profiles: $profiles); } #[AsTask(description: 'Lists containers status', aliases: ['ps'])] function ps(bool $ports = false): void { $command = [ 'ps', '--format', 'table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}\t{{.Command}}', '--no-trunc', ]; if ($ports) { $command[2] .= '\t{{.Ports}}'; } docker_compose($command, profiles: ['*']); if (!$ports) { io()->comment('You can use the "--ports" option to display ports.'); } } #[AsTask(description: 'Cleans the infrastructure (remove container, volume, networks)', aliases: ['destroy'])] function destroy( #[AsOption(description: 'Force the destruction without confirmation', shortcut: 'f')] bool $force = false, ): void { io()->title('Destroying infrastructure'); if (!$force) { io()->warning('This will permanently remove all containers, volumes, networks... created for this project.'); io()->note('You can use the --force option to avoid this confirmation.'); if (!io()->confirm('Are you sure?', false)) { io()->comment('Aborted.'); return; } } docker_compose(['down', '--remove-orphans', '--volumes', '--rmi=local'], profiles: ['*']); $files = finder() ->in(variable('root_dir') . '/infrastructure/docker/services/router/certs/') ->name('*.pem') ->files() ; fs()->remove($files); } #[AsTask(description: 'Generates SSL certificates (with mkcert if available or self-signed if not)')] function generate_certificates( #[AsOption(description: 'Force the certificates re-generation without confirmation', shortcut: 'f')] bool $force = false, ): void { $sslDir = variable('root_dir') . '/infrastructure/docker/services/router/certs'; if (file_exists("{$sslDir}/cert.pem") && !$force) { io()->comment('SSL certificates already exists.'); io()->note('Run "castor docker:generate-certificates --force" to generate new certificates.'); return; } io()->title('Generating SSL certificates'); if ($force) { if (file_exists($f = "{$sslDir}/cert.pem")) { io()->comment('Removing existing certificates in infrastructure/docker/services/router/certs/*.pem.'); unlink($f); } if (file_exists($f = "{$sslDir}/key.pem")) { unlink($f); } } $finder = new ExecutableFinder(); $mkcert = $finder->find('mkcert'); if ($mkcert) { $pathCaRoot = capture(['mkcert', '-CAROOT']); if (!is_dir($pathCaRoot)) { io()->warning('You must have mkcert CA Root installed on your host with "mkcert -install" command.'); return; } $rootDomain = variable('root_domain'); run([ 'mkcert', '-cert-file', "{$sslDir}/cert.pem", '-key-file', "{$sslDir}/key.pem", $rootDomain, "*.{$rootDomain}", ...variable('extra_domains'), ]); io()->success('Successfully generated SSL certificates with mkcert.'); if ($force) { io()->note('Please restart the infrastructure to use the new certificates with "castor up" or "castor start".'); } return; } run(['infrastructure/docker/services/router/generate-ssl.sh'], context: context()->withQuiet()); io()->success('Successfully generated self-signed SSL certificates in infrastructure/docker/services/router/certs/*.pem.'); io()->comment('Consider installing mkcert to generate locally trusted SSL certificates and run "castor docker:generate-certificates --force".'); if ($force) { io()->note('Please restart the infrastructure to use the new certificates with "castor up" or "castor start".'); } } #[AsTask(description: 'Starts the workers', namespace: 'docker:worker', name: 'start', aliases: ['start-workers'])] function workers_start(): void { io()->title('Starting workers'); $command = ['up', '--detach', '--wait', '--no-build']; $profiles = ['worker', 'default']; try { docker_compose($command, profiles: $profiles); } catch (ProcessFailedException $e) { preg_match('/service "(\w+)" depends on undefined service "(\w+)"/', $e->getProcess()->getErrorOutput(), $matches); if (!$matches) { throw $e; } $r = new \ReflectionFunction(__FUNCTION__); io()->newLine(); io()->error('An error occurred while starting the workers.'); io()->warning(\sprintf( <<<'EOT' The "%1$s" service depends on the "%2$s" service, which is not defined in the current docker-compose configuration. Usually, this means that the service "%2$s" is not defined in the same profile (%3$s) as the "%1$s" service. You can try to add its profile in the current task: %4$s:%5$s EOT, $matches[1], $matches[2], implode(', ', $profiles), PathHelper::makeRelative((string) $r->getFileName()), $r->getStartLine(), )); } } #[AsTask(description: 'Stops the workers', namespace: 'docker:worker', name: 'stop', aliases: ['stop-workers'])] function workers_stop(): void { io()->title('Stopping workers'); // Docker compose cannot stop a single service in a profile, if it depends // on another service in another profile. To make it work, we need to select // both profiles, and so stop both services // So we find all services, in all profiles, and manually filter the one // that has the "worker" profile, then we stop it $command = ['stop']; foreach (get_services() as $name => $service) { foreach ($service['profiles'] ?? [] as $profile) { if ('worker' === $profile) { $command[] = $name; continue 2; } } } docker_compose($command, profiles: ['*']); } /** * @param list $subCommand * @param list $profiles */ function docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process { $c ??= context(); $profiles = $profiles ?: ['default']; $domains = [$c['root_domain'], ...$c['extra_domains']]; $domains = '`' . implode('`) || Host(`', $domains) . '`'; $c = $c->withEnvironment([ 'PROJECT_NAME' => $c['project_name'], 'PROJECT_ROOT_DOMAIN' => $c['root_domain'], 'PROJECT_DOMAINS' => $domains, 'USER_ID' => $c['user_id'], 'PHP_VERSION' => $c['php_version'], 'REGISTRY' => $c['registry'] ?? '', ]); if ($c['APP_ENV'] ?? false) { $c = $c->withEnvironment([ 'APP_ENV' => $c['APP_ENV'] ?? '', ]); } $command = [ 'docker', 'compose', '-p', $c['project_name'], ]; foreach ($profiles as $profile) { $command[] = '--profile'; $command[] = $profile; } foreach ($c['docker_compose_files'] as $file) { $command[] = '-f'; $command[] = $c['root_dir'] . '/infrastructure/docker/' . $file; } $command = array_merge($command, $subCommand); return run($command, context: $c); } function docker_compose_run( string $runCommand, ?Context $c = null, string $service = 'builder', bool $noDeps = true, ?string $workDir = null, bool $portMapping = false, ): Process { $c ??= context(); $command = [ 'run', '--rm', ]; if ($noDeps) { $command[] = '--no-deps'; } if ($portMapping) { $command[] = '--service-ports'; } if (null !== $workDir) { $command[] = '-w'; $command[] = $workDir; } foreach ($c['docker_compose_run_environment'] as $key => $value) { $command[] = '-e'; $command[] = "{$key}={$value}"; } $command[] = $service; $command[] = '/bin/bash'; $command[] = '-c'; $command[] = "{$runCommand}"; return docker_compose($command, c: $c, profiles: ['*']); } function docker_exit_code( string $runCommand, ?Context $c = null, string $service = 'builder', bool $noDeps = true, ?string $workDir = null, ): int { $c = ($c ?? context())->withAllowFailure(); $process = docker_compose_run( runCommand: $runCommand, c: $c, service: $service, noDeps: $noDeps, workDir: $workDir, ); return $process->getExitCode() ?? 0; } // Mac users have a lot of problems running Yarn / Webpack on the Docker stack // so this func allow them to run these tools on their host function run_in_docker_or_locally_for_mac(string $command, ?Context $c = null): void { $c ??= context(); if ($c['macos']) { run($command, context: $c->withWorkingDirectory($c['root_dir'])); } else { docker_compose_run($command, c: $c); } } #[AsTask(description: 'Push images cache to the registry', namespace: 'docker', name: 'push', aliases: ['push'])] function push(bool $dryRun = false): void { $registry = variable('registry'); if (!$registry) { throw new \RuntimeException('You must define a registry to push images.'); } // Generate bake file $targets = []; foreach (get_services() as $service => $config) { $cacheFrom = $config['build']['cache_from'][0] ?? null; if (null === $cacheFrom) { continue; } $cacheFrom = explode(',', $cacheFrom); $reference = null; $type = null; if (1 === \count($cacheFrom)) { $reference = $cacheFrom[0]; $type = 'registry'; } else { foreach ($cacheFrom as $part) { $from = explode('=', $part); if (2 !== \count($from)) { continue; } if ('type' === $from[0]) { $type = $from[1]; } if ('ref' === $from[0]) { $reference = $from[1]; } } } $targets[] = [ 'reference' => $reference, 'type' => $type, 'context' => $config['build']['context'], 'dockerfile' => $config['build']['dockerfile'] ?? 'Dockerfile', 'target' => $config['build']['target'] ?? null, ]; } $content = \sprintf(<<<'EOHCL' group "default" { targets = [%s] } EOHCL , implode(', ', array_map(fn ($target) => \sprintf('"%s"', $target['target']), $targets))); foreach ($targets as $target) { $content .= \sprintf(<<<'EOHCL' target "%s" { context = "%s" dockerfile = "%s" cache-from = ["%s"] cache-to = ["type=%s,ref=%s,mode=max"] target = "%s" args = { PHP_VERSION = "%s" } } EOHCL , $target['target'], $target['context'], $target['dockerfile'], $target['reference'], $target['type'], $target['reference'], $target['target'], variable('php_version')); } if ($dryRun) { io()->write($content); return; } // write bake file in tmp file $bakeFile = tempnam(sys_get_temp_dir(), 'bake'); file_put_contents($bakeFile, $content); // Run bake run(['docker', 'buildx', 'bake', '-f', $bakeFile]); } /** * @return array, build: array{context: string, dockerfile?: string, cache_from?: list, target?: string}}> */ function get_services(): array { return json_decode( docker_compose( ['config', '--format', 'json'], context()->withQuiet(), profiles: ['*'], )->getOutput(), true, )['services']; } /** * @return string[] */ function get_service_names(): array { return array_keys(get_services()); } ================================================ FILE: .castor/init.php ================================================ remove([ '.github/', 'README.md', 'CHANGELOG.md', 'CONTRIBUTING.md', 'LICENSE', __FILE__, ]); fs()->rename('README.dist.md', 'README.md'); $readMeContent = file_get_contents('README.md'); if (false === $readMeContent) { return; } $urls = [variable('root_domain'), ...variable('extra_domains')]; $readMeContent = str_replace('', implode(' ', $urls), $readMeContent); file_put_contents('README.md', $readMeContent); } #[AsTask(description: 'Install Symfony')] function symfony(bool $webApp = false): void { $base = rtrim(variable('root_dir') . '/application'); $gitIgnore = $base . '/.gitignore'; $gitIgnoreContent = ''; if (file_exists($gitIgnore)) { $gitIgnoreContent = file_get_contents($gitIgnore); } build(); docker_compose_run('composer create-project symfony/skeleton sf'); fs()->mirror($base . '/sf/', $base); fs()->remove([$base . '/sf', $base . '/var']); if ($webApp) { docker_compose_run('composer require webapp'); } docker_compose_run("sed -i 's#^DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\\?serverVersion=16\\&charset=utf8#' .env"); file_put_contents($gitIgnore, $gitIgnoreContent, \FILE_APPEND); } ================================================ FILE: .castor/qa.php ================================================ title('Installing QA tooling'); docker_compose_run('composer install -o', workDir: '/var/www/tools/php-cs-fixer'); docker_compose_run('composer install -o', workDir: '/var/www/tools/phpstan'); docker_compose_run('composer install -o', workDir: '/var/www/tools/twig-cs-fixer'); } #[AsTask(description: 'Updates tooling')] function update(): void { io()->title('Updating QA tooling'); docker_compose_run('composer update -o', workDir: '/var/www/tools/php-cs-fixer'); docker_compose_run('composer update -o', workDir: '/var/www/tools/phpstan'); docker_compose_run('composer update -o', workDir: '/var/www/tools/twig-cs-fixer'); } // /** // * @param string[] $rawTokens // */ // #[AsTask(description: 'Runs PHPUnit', aliases: ['phpunit'])] // function phpunit(#[AsRawTokens] array $rawTokens = []): int // { // io()->section('Running PHPUnit...'); // // return docker_exit_code('bin/phpunit ' . implode(' ', $rawTokens)); // } #[AsTask(description: 'Runs PHPStan', aliases: ['phpstan'])] function phpstan( #[AsOption(description: 'Generate baseline file', shortcut: 'b')] bool $baseline = false, ): int { if (!is_dir(variable('root_dir') . '/tools/phpstan/vendor')) { install(); } io()->section('Running PHPStan...'); $options = $baseline ? '--generate-baseline --allow-empty-baseline' : ''; $command = \sprintf('phpstan analyse --memory-limit=-1 %s -v', $options); return docker_exit_code($command, workDir: '/var/www'); } #[AsTask(description: 'Fixes Coding Style', aliases: ['cs'])] function cs(bool $dryRun = false): int { if (!is_dir(variable('root_dir') . '/tools/php-cs-fixer/vendor')) { install(); } io()->section('Running PHP CS Fixer...'); if ($dryRun) { return docker_exit_code('php-cs-fixer fix --dry-run --diff', workDir: '/var/www'); } return docker_exit_code('php-cs-fixer fix', workDir: '/var/www'); } #[AsTask(description: 'Fixes Twig Coding Style', aliases: ['twig-cs'])] function twigCs(bool $dryRun = false): int { if (!is_dir(variable('root_dir') . '/tools/twig-cs-fixer/vendor')) { install(); } io()->section('Running Twig CS Fixer...'); if ($dryRun) { return docker_exit_code('twig-cs-fixer', workDir: '/var/www'); } return docker_exit_code('twig-cs-fixer --fix', workDir: '/var/www'); } ================================================ FILE: .gitattributes ================================================ # Force LF line ending (mandatory for Windows) * text=auto eol=lf ================================================ FILE: .github/workflows/cache.yml ================================================ name: Push docker image to registry "on": push: # Only run this job when pushing to the main branch branches: ["main"] permissions: contents: read packages: write env: DS_REGISTRY: "ghcr.io/jolicode/docker-starter" DS_PHP_VERSION: "8.5" jobs: push-images: name: Push image to registry runs-on: ubuntu-latest steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to registry shell: bash run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - name: setup-castor uses: castor-php/setup-castor@v1.0.0 - uses: actions/checkout@v6 - name: "Build and start the infrastructure" run: "castor docker:push" ================================================ FILE: .github/workflows/ci.yml ================================================ name: Continuous Integration "on": push: branches: ["main"] pull_request: branches: ["main"] schedule: - cron: "0 0 * * MON" permissions: contents: read packages: read env: # Fix for symfony/color detection. We know GitHub Actions can handle it ANSICON: 1 CASTOR_CONTEXT: ci DS_REGISTRY: "ghcr.io/jolicode/docker-starter" jobs: check-dockerfiles: name: Check Dockerfile runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Check php/Dockerfile uses: hadolint/hadolint-action@v3.3.0 with: dockerfile: infrastructure/docker/services/php/Dockerfile ci: name: Test with PHP ${{ matrix.php-version }} strategy: fail-fast: false matrix: php-version: ["8.3", "8.4", "8.5"] runs-on: ubuntu-latest env: DS_PHP_VERSION: ${{ matrix.php-version }} steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to registry shell: bash run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - name: setup-castor uses: castor-php/setup-castor@v1.0.0 - uses: actions/checkout@v6 - name: "Build and start the infrastructure" run: "castor start" - name: "Check PHP coding standards" run: "castor qa:cs --dry-run" - name: "Run PHPStan" run: "castor qa:phpstan" - name: "Test HTTP server" run: | set -e set -o pipefail curl --fail --insecure --silent -H "Host: app.test" https://127.0.0.1 | grep "Hello world" curl --fail --insecure --silent -H "Host: app.test" https://127.0.0.1 | grep "${{ matrix.php-version }}" - name: "Test builder" run: | set -e set -o pipefail cat > .castor/test.php <<'EOPHP' application/public/index.php <<'EOPHP' exec('CREATE TABLE test (id integer NOT NULL)'); $pdo->exec('INSERT INTO test VALUES (1)'); echo $pdo->query('SELECT * from test')->fetchAll() ? 'database OK' : 'database KO'; EOPHP # FPM seems super slow to detect the change, we need to wait a bit sleep 3 curl --fail --insecure --silent -H "Host: app.test" https://127.0.0.1 | grep "database OK" ================================================ FILE: .gitignore ================================================ /.castor.stub.php /infrastructure/docker/docker-compose.override.yml /infrastructure/docker/services/router/certs/*.pem # Tools .php-cs-fixer.cache .twig-cs-fixer.cache ================================================ FILE: .home/.gitignore ================================================ /* !.gitignore ================================================ FILE: .php-cs-fixer.php ================================================ ignoreVCSIgnored(true) ->ignoreDotFiles(false) ->in(__DIR__) ->append([ __FILE__, ]) ; return (new PhpCsFixer\Config()) ->setUnsupportedPhpVersionAllowed(true) ->setRiskyAllowed(true) ->setRules([ '@PHP83Migration' => true, '@PhpCsFixer' => true, '@Symfony' => true, '@Symfony:risky' => true, 'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it 'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it 'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it 'concat_space' => ['spacing' => 'one'], 'ordered_class_elements' => true, // Symfony(PSR12) override the default value, but we don't want 'blank_line_before_statement' => true, // Symfony(PSR12) override the default value, but we don't want ]) ->setFinder($finder) ; ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG ## 4.0.0 (not released yet) * Tooling * Migrate from Invoke to Castor * Add `castor symfony` to install a Symfony application * Add `castor init` command to initialize a new project * Add a `test` context * Services * Upgrade Traefik from v2.7 to v3.0 * Upgrade to PostgreSQL v16 * Replace maildev with Mailpit * PHP * Drop support for PHP < 8.3 * Add support for PHP 8.4, 8.5 * Add support for pie * Add some PHP tooling (PHP-CS-Fixer, PHPStan, Twig-CS-Fixer) * Update Composer to version 2.8 * Builder * Update NodeJS to version 20.x LTS * Add support for autocomplete (composer & symfony) * Docker: * Add a dockerfile linter * Do not store certificates in the router image * Do not hardcode a user in the Dockerfile (and so the image), map it dynamically * Mount the project in `/var/www` instead of `/home/app` * Add support for caching image cache in a registry * Upgrade base to Debian Bookworm (12.5) ## 3.11.0 (2023-05-30) * Use docker stages to build images ## 3.10.0 (2023-05-22) * Fix workers detection in docker v23 * Update Invoke to version 2.1 * Update Composer to version 2.5.5 * Upgrade NodeJS to 18.x LTS version * Migrate to Compose V2 ## 3.9.0 (2022-12-21) * Update documentation cookbook for installing redirection.io and Blackfire to remove deprecated apt-key usage * Update Composer to version 2.5.0 * Increase the number of FPM worker from 4 to 25 * Enabled PHP FPM status page on `/php-fpm-status` * Added support for PHP 8.2 ## 3.8.0 (2022-06-15) * Add documentation cookbook for using pg_activity * Forward CI env vars in Docker containers * Run the npm/yarn/webpack commands on the host for all mac users (even the ones not using Dinghy) * Tests with PHP 7.4, 8.0, and 8.1 ## 3.7.0 (2022-05-24) * Add documentation cookbook for installing redirection.io * Upgrade to Traefik v2.7.0 * Upgrade to PostgreSQL v14 * Upgrade to Composer v2.3 ## 3.6.0 (2022-03-10) * Upgrade NodeJS to version 16.x LTS and remove deprecated apt-key usage * Various fix in the documentation * Remove certificates when destroying infrastructure ## 3.5.0 (2022-01-27) * Update PHP to version 8.1 * Generate SSL certificates with mkcert when available (self-signed otherwise) ## 3.4.0 (2021-10-13) * Fix `COMPOSER_CACHE_DIR` default value when composer is not installed on host * Upgrade base to Debian Bullseye (11.0) * Document webpack 5+ integration ## 3.3.0 (2021-06-03) * Update PHP to version 8.0 * Update Composer to version 2.1.0 * Fix APT key for Sury repository * Fix the version of our debian base image ## 3.2.0 (2021-02-17) * Migrate CI from Circle to GitHub Actions * Add support for `docker-compose.override.yml` ## 3.1.0 (2020-11-13) * Fix TTY on all OS * Add default vendor installation command with auto-detection in `install()` for Yarn, NPM and Composer * Update Composer to version 2 * Install by default php-uuid extension * Update NodeJS from 12.x to 14.x ## 3.0.0 (2020-07-01) * Migrate from Fabric to Invoke * Migrate from Alpine to Debian for PHP images * Add a confirmation when calling `inv destroy` * Tweak the PHP configuration * Upgrade PostgreSQL from 11 to 12 * Upgrade Traefik from 2.0 to 2.2 * Add an `help` task. This is the default one * The help command list all HTTP(s) services available * The help command list tasks available * Fix the support for Mac and Windows * Try to map the correct Composer cache dir from the host * Enhance the documentation ## 2.0.0 (2020-01-08) * Better Docker for Windows support * Add support for running many projects at the same time * Upgrade Traefik from 1.7 to 2.0 * Add native support for workers ## 1.0.0 (2019-07-27) * First release ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing First of all, **thank you** for contributing, **you are awesome**! Everybody should be able to help. Here's how you can do it: 1. [Fork it](https://github.com/jolicode/docker-starter/fork_select) 2. improve it 3. submit a [pull request](https://help.github.com/articles/creating-a-pull-request) Here's some tips to make you the best contributor ever: * [Rules](#rules) * [Keeping your fork up-to-date](#keeping-your-fork-up-to-date) ## Rules Here are a few rules to follow in order to ease code reviews, and discussions before maintainers accept and merge your work. Please, write [commit messages that make sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) before submitting your Pull Request (see also how to [keep your fork up-to-date](#keeping-your-fork-up-to-date)). One may ask you to [squash your commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) too. This is used to "clean" your Pull Request before merging it (we don't want commits such as `fix tests`, `fix 2`, `fix 3`, etc.). Also, while creating your Pull Request on GitHub, you MUST write a description which gives the context and/or explains why you are creating it. Your work will then be reviewed as soon as possible (suggestions about some changes, improvements or alternatives may be given). ## Keeping your fork up-to-date To keep your fork up-to-date, you should track the upstream (original) one using the following command: ```shell git remote add upstream https://github.com/jolicode/docker-starter.git ``` Then get the upstream changes: ```shell git checkout master git pull --rebase upstream master git checkout git rebase master ``` Finally, publish your changes: ```shell git push -f origin ``` Your pull request will be automatically updated. Thank you! ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019-present JoliCode Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.dist.md ================================================ # My project ## Running the application locally ### Requirements A Docker environment is provided and requires you to have these tools available: * Docker * Bash * [Castor](https://github.com/jolicode/castor#installation) #### Castor Once `castor` is installed, in order to improve your usage of `castor` scripts, you can install console autocompletion script. If you are using bash: ```bash castor completion | sudo tee /etc/bash_completion.d/castor ``` If you are using something else, please refer to your shell documentation. You may need to use `castor completion > /to/somewhere`. `castor` supports completion for `bash`, `zsh` & `fish` shells. ### Docker environment The Docker infrastructure provides a web stack with: - NGINX - PostgreSQL - PHP - Traefik - A container with some tooling: - Composer - Node - Yarn / NPM ### Domain configuration (first time only) Before running the application for the first time, ensure your domain names point the IP of your Docker daemon by editing your `/etc/hosts` file. This IP is probably `127.0.0.1` unless you run Docker in a special VM (like docker-machine for example). > [!NOTE] > The router binds port 80 and 443, that's why it will work with `127.0.0.1` ``` echo '127.0.0.1 ' | sudo tee -a /etc/hosts ``` ### Starting the stack Launch the stack by running this command: ```bash castor start ``` > [!NOTE] > the first start of the stack should take a few minutes. The site is now accessible at the hostnames you have configured over HTTPS (you may need to accept self-signed SSL certificate if you do not have `mkcert` installed on your computer - see below). ### SSL certificates HTTPS is supported out of the box. SSL certificates are not versioned and will be generated the first time you start the infrastructure (`castor start`) or if you run `castor docker:generate-certificates`. If you have `mkcert` installed on your computer, it will be used to generate locally trusted certificates. See [`mkcert` documentation](https://github.com/FiloSottile/mkcert#installation) to understand how to install it. Do not forget to install CA root from `mkcert` by running `mkcert -install`. If you don't have `mkcert`, then self-signed certificates will instead be generated with `openssl`. You can configure [infrastructure/docker/services/router/openssl.cnf](infrastructure/docker/services/router/openssl.cnf) to tweak certificates. You can run `castor docker:generate-certificates --force` to recreate new certificates if some were already generated. Remember to restart the infrastructure to make use of the new certificates with `castor build && castor up` or `castor start`. ### Builder Having some composer, yarn or other modifications to make on the project? Start the builder which will give you access to a container with all these tools available: ```bash castor builder ``` ### Other tasks Checkout `castor` to have the list of available tasks. ================================================ FILE: README.md ================================================

Docker Starter
Docker Starter
A docker-based infrastructure wrapped in an easy-to-use command line, oriented for PHP projects.

This repository contains a collection of Dockerfile and docker-compose configurations for your PHP projects with built-in support for HTTPS, custom domain, databases, workers... and is used as a foundation for our projects at [JoliCode](https://jolicode.com/). > [!WARNING] > You are reading the README of version 4 that uses [Castor](https://castor.jolicode.com). * If you are using [Invoke](https://www.pyinvoke.org/), you can read the [dedicated README](https://github.com/jolicode/docker-starter/tree/v3.11.0); * If you are using [Fabric](https://www.fabfile.org/), you can read the [dedicated README](https://github.com/jolicode/docker-starter/tree/v2.0.0); ## Project configuration Before executing any command, you need to configure a few parameters in the `castor.php` file, in the `create_default_variables()` function: * `project_name` (**required**): This will be used to prefix all docker objects (network, images, containers); * `root_domain` (optional, default: `project_name + '.test'`): This is the root domain where the application will be available; * `extra_domains` (optional): This contains extra domains where the application will be available; * `php_version` (optional, default: `8.5`): This is PHP version. For example: ```php function create_default_variables(): array { $projectName = 'app'; $tld = 'test'; return [ 'project_name' => $projectName, 'root_domain' => "{$projectName}.{$tld}", 'extra_domains' => [ "www.{$projectName}.{$tld}", "admin.{$projectName}.{$tld}", "api.{$projectName}.{$tld}", ], 'php_version' => 8.3, ]; ) ``` Will give you `https://app.test`, `https://www.app.test`, `https://api.app.test` and `https://admin.app.test` pointing at your `application/` directory. > [!NOTE] > Some castor tasks have been added for DX purposes. Checkout and adapt > the tasks `install`, `migrate` and `cache_clear` to your project. ## Usage documentation We provide a [README.dist.md](./README.dist.md) to bootstrap your project documentation, with everything you need to know to start and interact with the infrastructure. If you want to install a Symfony project, you can run (before `castor init`): ``` castor symfony [--web-app] ``` To replace this README with the dist, and remove all unnecessary files, you can run: ```bash castor init ``` > [!NOTE] > This command can be run only once Also, in order to improve your usage of castor scripts, you can install console autocompletion script. If you are using bash: ```bash castor completion | sudo tee /etc/bash_completion.d/castor ``` If you are using something else, please refer to your shell documentation. You may need to use `castor completion > /to/somewhere`. Castor supports completion for `bash`, `zsh` & `fish` shells. ## Cookbooks ### How to install third party tools with Composer
Read the cookbook If you want to install some third party tools with Composer, it is recommended to install them in their dedicated directory. PHPStan and PHP-CS-Fixer are already installed in the `tools` directory. We suggest to: 1. create a composer.json which requires only this tool in `tools//composer.json`; 1. create an executable symbolic link to the tool from the root directory of the project: `ln -s ..//vendor/bin/ tools/bin/`; > [!NOTE] > Relative symlinks works here, because the first part of the command is relative to the second part, not to the current directory. Since `tools/bin` path is appended to the `$PATH`, tools will be available globally in the builder container.
### How to change the layout of the project
Read the cookbook If you want to rename the `application` directory, or even move its content to the root directory, you have to edit each reference to it. Theses references represent each application entry point, whether it be over HTTP or CLI. Usually, there is three places where you need to do it: * In Nginx configuration file: `infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf`. You need to update `http.server.root` option to the new path. For example: ```diff - root /var/www/application/public; + root /var/www/public; ``` * In all workers configuration file: `infrastructure/docker/docker-compose.worker.yml`: ```diff - command: php -d memory_limit=1G /var/www/application/bin/console messenger:consume async --memory-limit=128M + command: php -d memory_limit=1G /var/www/bin/console messenger:consume async --memory-limit=128M ``` * In the builder, to land in the right directory directly: `infrastructure/docker/services/php/Dockerfile`: ```diff - WORKDIR /var/www/application + WORKDIR /var/www ```
### How to use MariaDB instead of PostgreSQL
Read the cookbook In order to use MariaDB, you will need to apply this patch: ```diff diff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml index d00f315..bdfdc65 100644 --- a/infrastructure/docker/docker-compose.builder.yml +++ b/infrastructure/docker/docker-compose.builder.yml @@ -10,7 +10,7 @@ services: builder: build: services/builder depends_on: - - postgres + - mariadb environment: - COMPOSER_MEMORY_LIMIT=-1 volumes: diff --git a/infrastructure/docker/docker-compose.worker.yml b/infrastructure/docker/docker-compose.worker.yml index 2eda814..59f8fed 100644 --- a/infrastructure/docker/docker-compose.worker.yml +++ b/infrastructure/docker/docker-compose.worker.yml @@ -5,7 +5,7 @@ x-services-templates: worker_base: &worker_base build: services/worker depends_on: - - postgres + - mariadb #- rabbitmq volumes: - "../..:/var/www:cached" diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml index 49a2661..1804a01 100644 --- a/infrastructure/docker/docker-compose.yml +++ b/infrastructure/docker/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.7' volumes: - postgres-data: {} + mariadb-data: {} services: router: @@ -13,7 +13,7 @@ services: frontend: build: services/frontend depends_on: - - postgres + - mariadb volumes: - "../..:/var/www:cached" labels: @@ -24,10 +24,7 @@ services: # Comment the next line to be able to access frontend via HTTP instead of HTTPS - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file" - postgres: - image: postgres:16 - environment: - - POSTGRES_USER=app - - POSTGRES_PASSWORD=app + mariadb: + image: mariadb:11 + environment: + - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1 + healthcheck: + test: "mariadb-admin ping -h localhost" + interval: 5s + timeout: 5s + retries: 10 volumes: - - postgres-data:/var/lib/postgresql/data + - mariadb-data:/var/lib/mysql diff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile index 56e1835..95fee78 100644 --- a/infrastructure/docker/services/php/Dockerfile +++ b/infrastructure/docker/services/php/Dockerfile @@ -24,7 +24,7 @@ RUN apk add --no-cache \ php${PHP_VERSION}-intl \ php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-pgsql \ + php${PHP_VERSION}-mysql \ php${PHP_VERSION}-xml \ php${PHP_VERSION}-zip \ ```
### How to use MySQL instead of PostgreSQL
Read the cookbook In order to use MySQL, you will need to apply this patch: ```diff diff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml index d00f315..bdfdc65 100644 --- a/infrastructure/docker/docker-compose.builder.yml +++ b/infrastructure/docker/docker-compose.builder.yml @@ -10,7 +10,7 @@ services: builder: build: services/builder depends_on: - - postgres + - mysql environment: - COMPOSER_MEMORY_LIMIT=-1 volumes: diff --git a/infrastructure/docker/docker-compose.worker.yml b/infrastructure/docker/docker-compose.worker.yml index 2eda814..59f8fed 100644 --- a/infrastructure/docker/docker-compose.worker.yml +++ b/infrastructure/docker/docker-compose.worker.yml @@ -5,7 +5,7 @@ x-services-templates: worker_base: &worker_base build: services/worker depends_on: - - postgres + - mysql #- rabbitmq volumes: - "../..:/var/www:cached" diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml index 49a2661..1804a01 100644 --- a/infrastructure/docker/docker-compose.yml +++ b/infrastructure/docker/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.7' volumes: - postgres-data: {} + mysql-data: {} services: router: @@ -13,7 +13,7 @@ services: frontend: build: services/frontend depends_on: - - postgres + - mysql volumes: - "../..:/var/www:cached" labels: @@ -24,10 +24,7 @@ services: # Comment the next line to be able to access frontend via HTTP instead of HTTPS - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file" - postgres: - image: postgres:16 - environment: - - POSTGRES_USER=app - - POSTGRES_PASSWORD=app + mysql: + image: mysql:8 + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=1 + healthcheck: + test: "mysqladmin ping -h localhost" + interval: 5s + timeout: 5s + retries: 10 volumes: - - postgres-data:/var/lib/postgresql/data + - mysql-data:/var/lib/mysql diff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile index 56e1835..95fee78 100644 --- a/infrastructure/docker/services/php/Dockerfile +++ b/infrastructure/docker/services/php/Dockerfile @@ -24,7 +24,7 @@ RUN apk add --no-cache \ php${PHP_VERSION}-intl \ php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-pgsql \ + php${PHP_VERSION}-mysql \ php${PHP_VERSION}-xml \ php${PHP_VERSION}-zip \ ```
### How to use with Webpack Encore
Read the cookbook > [!NOTE] > this cookbook documents the integration of webpack 5+. For older version > of webpack, use previous version of the docker starter. If you want to use Webpack Encore in a Symfony project, 1. Follow [instructions on symfony.com](https://symfony.com/doc/current/frontend/encore/installation.html#installing-encore-in-symfony-applications) to install webpack encore. You will need to follow [these instructions](https://symfony.com/doc/current/frontend/encore/simple-example.html) too. 2. Create a new service for encore: Add the following content to the `docker-compose.yml` file: ```yaml services: encore: build: context: services/php target: builder volumes: - "../..:/var/www:cached" command: > yarn run dev-server --hot --host 0.0.0.0 --public https://encore.${PROJECT_ROOT_DOMAIN} --allowed-hosts ${PROJECT_ROOT_DOMAIN} --allowed-hosts encore.${PROJECT_ROOT_DOMAIN} --client-web-socket-url-hostname encore.${PROJECT_ROOT_DOMAIN} --client-web-socket-url-port 443 --client-web-socket-url-protocol wss --server-type http labels: - "project-name=${PROJECT_NAME}" - "traefik.enable=true" - "traefik.http.routers.${PROJECT_NAME}-encore.rule=Host(`encore.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-encore.tls=true" - "traefik.http.services.encore.loadbalancer.server.port=8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/build/app.css"] profiles: - default ``` If the assets are not reachable, you may accept self-signed certificate. To do so, open a new tab at https://encore.app.test and click on accept.
### How to use with AssetMapper
Read the cookbook 1. Follow [instructions on symfony.com](https://symfony.com/doc/current/frontend/asset_mapper.html#installation) to install AssetMapper. 1. Remove this block in the `infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` file: ``` location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ { access_log off; add_header Cache-Control "no-cache"; } ``` 1. Remove these lines in the `infrastructure/docker/services/php/Dockerfile` file: ```diff SHELL ["/bin/bash", "-o", "pipefail", "-c"] - ARG NODEJS_VERSION=18.x - RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor > /usr/share/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODEJS_VERSION} bullseye main" > /etc/apt/sources.list.d/nodejs.list # Default toys RUN apt-get update \ && apt-get install -y --no-install-recommends \ git \ make \ - nodejs \ sudo \ unzip \ && apt-get clean \ - && npm install -g yarn@1.22 \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* ```
### How to add Elasticsearch and Kibana
Read the cookbook In order to use Elasticsearch and Kibana, you should add the following content to the `docker-compose.yml` file: ```yaml volumes: elasticsearch-data: {} services: elasticsearch: image: elasticsearch:7.8.0 volumes: - elasticsearch-data:/usr/share/elasticsearch/data environment: - "discovery.type=single-node" labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-elasticsearch.rule=Host(`elasticsearch.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-elasticsearch.tls=true" healthcheck: test: "curl --fail http://localhost:9200/_cat/health || exit 1" interval: 5s timeout: 5s retries: 5 profiles: - default kibana: image: kibana:7.8.0 depends_on: - elasticsearch labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-kibana.rule=Host(`kibana.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-kibana.tls=true" profiles: - default ``` Then, you will be able to browse: * `https://kibana.` * `https://elasticsearch.` In your application, you can use the following configuration: * scheme: `http`; * host: `elasticsearch`; * port: `9200`.
### How to use with Sylius
Read the cookbook Add the php extension `gd` to `infrastructure/docker/services/php/Dockerfile` ``` php${PHP_VERSION}-gd \ ``` If you want to create a new Sylius project, you need to enter a builder (`inv builder`) and run the following commands 1. Remove the `application` folder: ```bash cd .. rm -rf application/* ``` 1. Create a new project: ```bash composer create-project sylius/sylius-standard application ``` 1. Configure the `.env` ```bash sed -i 's#DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\?serverVersion=12\&charset=utf8#' application/.env ```
### How to add RabbitMQ and its dashboard
Read the cookbook In order to use RabbitMQ and its dashboard, you should add a new service: ```Dockerfile # services/rabbitmq/Dockerfile FROM rabbitmq:3-management-alpine COPY etc/. /etc/ ``` And you can add specific RabbitMQ configuration in the `services/rabbitmq/etc/rabbitmq/rabbitmq.conf` file: ``` # services/rabbitmq/etc/rabbitmq/rabbitmq.conf vm_memory_high_watermark.absolute = 1GB ``` Finally, add the following content to the `docker-compose.yml` file: ```yaml volumes: rabbitmq-data: {} services: rabbitmq: build: services/rabbitmq volumes: - rabbitmq-data:/var/lib/rabbitmq labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-rabbitmq.rule=Host(`rabbitmq.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-rabbitmq.tls=true" - "traefik.http.services.rabbitmq.loadbalancer.server.port=15672" healthcheck: test: "rabbitmqctl eval '{ true, rabbit_app_booted_and_running } = { rabbit:is_booted(node()), rabbit_app_booted_and_running }, { [], no_alarms } = { rabbit:alarms(), no_alarms }, [] /= rabbit_networking:active_listeners(), rabbitmq_node_is_healthy.' || exit 1" interval: 5s timeout: 5s retries: 5 profiles: - default ``` In order to publish and consume messages with PHP, you need to install the `php${PHP_VERSION}-amqp` in the `php` image. Then, you will be able to browse: * `https://rabbitmq.` (username: `guest`, password: `guest`) In your application, you can use the following configuration: * host: `rabbitmq`; * username: `guest`; * password: `guest`; * port: `rabbitmq`. For example in Symfony you can use: `MESSENGER_TRANSPORT_DSN=amqp://guest:guest@rabbitmq:5672/%2f/messages`.
### How to add Redis and its dashboard
Read the cookbook In order to use Redis and its dashboard, you should add the following content to the `docker-compose.yml` file: ```yaml volumes: redis-data: {} redis-insight-data: {} services: redis: image: redis:5 healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 5 volumes: - "redis-data:/data" profiles: - default redis-insight: image: redislabs/redisinsight volumes: - "redis-insight-data:/db" labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-redis.rule=Host(`redis.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-redis.tls=true" profiles: - default ``` In order to communicate with Redis, you need to install the `php${PHP_VERSION}-redis` in the `php` image. Then, you will be able to browse: * `https://redis.` In your application, you can use the following configuration: * host: `redis`; * port: `6379`.
### How to add Mailpit
Read the cookbook In order to use Mailpit and its dashboard, you should add the following content to the `docker-compose.yml` file: ```yaml services: mail: image: axllent/mailpit environment: - MP_SMTP_BIND_ADDR=0.0.0.0:25 labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-mail.rule=Host(`mail.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-mail.tls=true" - "traefik.http.services.mail.loadbalancer.server.port=8025" profiles: - default ``` Then, you will be able to browse: * `https://mail.` In your application, you can use the following configuration: * scheme: `smtp`; * host: `mail`; * port: `25`. For example in Symfony you can use: `MAILER_DSN=smtp://mail:25`.
### How to add Mercure
Read the cookbook In order to use Mercure, you should add the following content to the `docker-compose.yml` file: ```yaml services: mercure: image: dunglas/mercure environment: - "MERCURE_PUBLISHER_JWT_KEY=password" - "MERCURE_SUBSCRIBER_JWT_KEY=password" - "ALLOW_ANONYMOUS=1" - "CORS_ALLOWED_ORIGINS=*" labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-mercure.rule=Host(`mercure.${PROJECT_ROOT_DOMAIN}`)" - "traefik.http.routers.${PROJECT_NAME}-mercure.tls=true" profiles: - default ``` If you are using Symfony, you must put the following configuration in the `.env` file: ``` MERCURE_PUBLISH_URL=http://mercure/.well-known/mercure MERCURE_JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6W10sInB1Ymxpc2giOltdfX0.t9ZVMwTzmyjVs0u9s6MI7-oiXP-ywdihbAfPlghTBeQ ```
### How to add redirection.io
Read the cookbook In order to use redirection.io, you should add the following content to the `docker-compose.yml` file to run the agent: ```yaml services: redirectionio-agent: build: services/redirectionio-agent ``` Add the following file `infrastructure/docker/services/redirectionio-agent/Dockerfile`: ```Dockerfile FROM alpine:3.12 AS alpine WORKDIR /tmp RUN apk add --no-cache wget ca-certificates \ && wget https://packages.redirection.io/dist/stable/2/any/redirectionio-agent-latest_any_amd64.tar.gz \ && tar -xzvf redirectionio-agent-latest_any_amd64.tar.gz FROM scratch # Binary copied from tar COPY --from=alpine /tmp/redirection-agent/redirectionio-agent /usr/local/bin/redirectionio-agent # Configuration, can be replaced by your own COPY etc /etc # Root SSL Certificates, needed as we do HTTPS requests to our service COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ CMD ["/usr/local/bin/redirectionio-agent"] ``` Add `infrastructure/docker/services/redirectionio-agent/etc/redirectionio/agent.yml`: ```yaml instance_name: "my-instance-dev" ### You may want to change this listen: 0.0.0.0:10301 ``` Then you'll need `wget`. In `infrastructure/docker/services/php/Dockerfile`, in stage `frontend`: ```Dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ wget \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* ``` You can group this command with another one. Then, **after** installing nginx, you need to install the module: ```Dockerfile RUN wget -q -O - https://packages.redirection.io/gpg.key | gpg --dearmor > /usr/share/keyrings/redirection.io.gpg \ && echo "deb [signed-by=/usr/share/keyrings/redirection.io.gpg] https://packages.redirection.io/deb/stable/2 focal main" | tee -a /etc/apt/sources.list.d/packages_redirection_io_deb.list \ && apt-get update \ && apt-get install libnginx-mod-redirectionio \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* ``` Finally, you need to edit `infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` to add the following configuration in the `server` block: ``` redirectionio_pass redirectionio-agent:10301; redirectionio_project_key "AAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBB"; ``` **Don't forget to change the project key**.
### How to add Blackfire.io
Read the cookbook In order to use Blackfire.io, you should add the following content to the `docker-compose.yml` file to run the agent: ```yaml services: blackfire: image: blackfire/blackfire environment: BLACKFIRE_SERVER_ID: FIXME BLACKFIRE_SERVER_TOKEN: FIXME BLACKFIRE_CLIENT_ID: FIXME BLACKFIRE_CLIENT_TOKEN: FIXME profiles: - default ``` Then you'll need `wget`. In `infrastructure/docker/services/php/Dockerfile`, in stage `base`: ```Dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ wget \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* ``` You can group this command with another one. Then, **after** installing PHP, you need to install the probe: ```Dockerfile RUN wget -q -O - https://packages.blackfire.io/gpg.key | gpg --dearmor > /usr/share/keyrings/blackfire.io.gpg \ && sh -c 'echo "deb [signed-by=/usr/share/keyrings/blackfire.io.gpg] http://packages.blackfire.io/debian any main" > /etc/apt/sources.list.d/blackfire.list' \ && apt-get update \ && apt-get install -y --no-install-recommends \ blackfire-php \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \ && sed -i 's#blackfire.agent_socket.*#blackfire.agent_socket=tcp://blackfire:8707#' /etc/php/${PHP_VERSION}/mods-available/blackfire.ini ``` If you want to profile HTTP calls, you need to enable the probe with PHP-FPM. So in `infrastructure/docker/services/php/Dockerfile`: ```Dockerfile RUN phpenmod blackfire ``` Here also, You can group this command with another one.
### How to add support for crons?
Read the cookbook In order to set up crontab, you should add a new container: ```Dockerfile # services/php/Dockerfile FROM php-base AS cron RUN apt-get update \ && apt-get install -y --no-install-recommends \ cron \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* COPY --link cron/crontab /etc/cron.d/crontab COPY --link cron/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["cron", "-f"] ``` And you can add all your crons in the `services/php/crontab` file: ```crontab * * * * * php -r 'echo time().PHP_EOL;' > /tmp/cron-stdout 2>&1 ``` And you can add the following content to the `services/php/entrypoint.sh` file: ```bash #!/bin/bash set -e groupadd -g $USER_ID app useradd -M -u $USER_ID -g $USER_ID -s /bin/bash app crontab -u app /etc/cron.d/crontab # Wrapper for logs FIFO=/tmp/cron-stdout rm -f $FIFO mkfifo $FIFO chmod 0666 $FIFO while true; do cat /tmp/cron-stdout done & exec "$@" ``` Finally, add the following content to the `docker-compose.yml` file: ```yaml services: cron: build: context: services/php target: cron cache_from: - "type=registry,ref=${REGISTRY:-}/cron:cache" # depends_on: # postgres: # condition: service_healthy env_file: .env environment: USER_ID: ${USER_ID} volumes: - "../..:/var/www:cached" - "../../.home:/home/app:cached" profiles: - default ```
### How to run workers?
Read the cookbook In order to set up workers, you should define their services in the `docker-compose.worker.yml` file: ```yaml services: worker_my_worker: <<: *worker_base command: /var/www/application/my-worker worker_date: <<: *worker_base command: watch -n 1 date ```
### How to use PHP FPM status page?
Read the cookbook If you want to use the [PHP FPM status page](https://www.php.net/manual/en/fpm.status.php) you need to remove a configuration block in the `infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` file: ```diff - # Remove this block if you want to access to PHP FPM monitoring - # dashboarsh (on URL: /php-fpm-status). WARNING: on production, you must - # secure this page (by user IP address, with a password, for example) - location ~ ^/php-fpm-status$ { - deny all; - } - ``` And if your application uses the front controller pattern, and you want to see the real request URI, you also need to uncomment the following configuration block: ```diff - # # Uncomment if you want to use /php-fpm-status endpoint **with** - # # real request URI. It may have some side effects, that's why it's - # # commented by default - # fastcgi_param SCRIPT_NAME $request_uri; + # Uncomment if you want to use /php-fpm-status endpoint **with** + # real request URI. It may have some side effects, that's why it's + # commented by default + fastcgi_param SCRIPT_NAME $request_uri; ```
### How to pg_activity for monitoring PostgreSQL
Read the cookbook In order to install pg_activity, you should add the following content to the `infrastructure/docker/services/postgres/Dockerfile` file: ```Dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ pg-activity \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* ``` Then, you can add the following content to the `castor.php` file: ```php #[AsTask(description: 'Monitor PostgreSQL', namespace: 'app:db')] function pg_activity(): void { docker_compose('exec postgres pg_activity -U app'); } ``` Finally you can use the following command: ``` castor app:db:pg-activity ```
### Docker For Windows support
Read the cookbook This starter kit is compatible with Docker for Windows, so you can enjoy native Docker experience on Windows. You will have to keep in mind some differences: - You will be prompted to run the env vars manually if you use PowerShell.
### How to access a container via hostname from another container
Read the cookbook Let's say you have a container (`frontend`) that responds to many hostnames: `app.test`, `api.app.test`, `admin.app.test`. And you have another container (`builder`) that needs to call the `frontend` with a specific hostname - or with HTTPS. This is usually the case when you have a functional test suite. To enable this feature, you need to add `extra_hosts` to the `builder` container like so: ```yaml services: builder: # [...] extra_hosts: - "app.test:host-gateway" - "api.app.test:host-gateway" - "admin.app.test:host-gateway" ```
### How to connect networks of two projects
Read the cookbook Let's say you have two projects `foo` and `bar`. You want to run both projects a the same time. And containers from `foo` project should be able to dialog with `bar` project via public network (host network). In the `foo` project, you'll need to declare the `bar_default` network in `docker-compose.yml`: ```yaml networks: bar_default: external: true ``` Then, attach it to the the `foo` router: ```yaml services: router: networks: - default - bar_default ``` Finally, you must remove the constraints on the router so it'll be able to discover containers from another docker compose project: ```diff --- a/infrastructure/docker/services/router/traefik/traefik.yaml +++ b/infrastructure/docker/services/router/traefik/traefik.yaml providers: docker: exposedByDefault: false - constraints: "Label(`project-name`,`{{ PROJECT_NAME }}`)" file: ``` Finally, you must : 1. build the project `foo` 1. build the project `bar` 1. Create the network `bar_default` (first time only) ``` docker network create bar_default ``` 1. start the project `foo` 1. start the project `bar`
### How to use FrankenPHP
Read the cookbook Migrating to FrankenPHP involves a lot of changes. You can take inspiration from the following [repostory](https://github.com/lyrixx/async-messenger-mercure) and specifically [this commit](https://github.com/lyrixx/async-messenger-mercure/commit/9ac8776253f3950a6c57d457b3742923f9e096a7).
### How to use a docker registry to cache images layer
Read the cookbook You can use a docker registry to cache images layer, it can be useful to speed up the build process during the CI and local development. First you need a docker registry, in following examples we will use the GitHub registry (ghcr.io). Then add the registry to the context variable of the `castor.php` file: ```php function create_default_variables(): Context { return [ // [...] 'registry' => 'ghcr.io/your-organization/your-project', ]; } ``` Once you have the registry, you can push the images to the registry: ```bash castor docker:push ``` > Pushing image cache from a dev environment to a registry is not recommended, > as cache may be sensitive to the environment and may not be compatible with > other environments. It happens, for example, when you add some build args > depending on your environment. It is recommended to push the cache from the CI > environment. This command will generate a bake file with the images to push from the `cache_from` directive of the `docker-compose.yml` file. If you want to add more images to push, you can add the `cache_from` directive to them. ```yaml services: my-service: build: cache_from: - "type=registry,ref=${REGISTRY:-}/my-service:cache" ``` #### How to use cached images in a GitHub action ##### Pushing images to the registry from a GitHub action 1. Ensure that the github token have the `write:packages` scope: ```yaml permissions: contents: read packages: write ``` 2. Install Docker buildx in the github action: ```yaml - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 ``` 3. Login to the registry: ```yaml - name: Log in to registry shell: bash run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin ``` ##### Using the cached images in GitHub action By default images are private in the GitHub registry, you will need to login to the registry to pull the images: ```yaml - name: Log in to registry shell: bash run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin ```
## Credits Docker-starter logo was created by [Caneco](https://twitter.com/caneco).

JoliCode is sponsoring this project
================================================ FILE: application/public/index.php ================================================ $projectName, 'root_domain' => "{$projectName}.{$tld}", 'extra_domains' => [ "www.{$projectName}.{$tld}", ], // In order to test docker stater, we need a way to pass different values. // You should remove the `$_SERVER` and hardcode your configuration. 'php_version' => $_SERVER['DS_PHP_VERSION'] ?? '8.5', 'registry' => $_SERVER['DS_REGISTRY'] ?? null, ]; } #[AsTask(description: 'Builds and starts the infrastructure, then install the application (composer, yarn, ...)')] function start(): void { io()->title('Starting the stack'); // workers_stop(); build(); install(); up(profiles: ['default']); // We can't start worker now, they are not installed migrate(); // workers_start(); notify('The stack is now up and running.'); io()->success('The stack is now up and running.'); about(); } #[AsTask(description: 'Installs the application (composer, yarn, ...)', namespace: 'app', aliases: ['install'])] function install(): void { io()->title('Installing the application'); $basePath = sprintf('%s/application', variable('root_dir')); if (is_file("{$basePath}/composer.json")) { io()->section('Installing PHP dependencies'); docker_compose_run('composer install -n --prefer-dist --optimize-autoloader'); } if (is_file("{$basePath}/yarn.lock")) { io()->section('Installing Node.js dependencies'); docker_compose_run('yarn install --frozen-lockfile'); } elseif (is_file("{$basePath}/package.json")) { io()->section('Installing Node.js dependencies'); if (is_file("{$basePath}/package-lock.json")) { docker_compose_run('npm ci'); } else { docker_compose_run('npm install'); } } if (is_file("{$basePath}/importmap.php")) { io()->section('Installing importmap'); docker_compose_run('bin/console importmap:install'); } qa\install(); } #[AsTask(description: 'Update dependencies')] function update(bool $withTools = false): void { io()->title('Updating dependencies...'); // docker_compose_run('composer update -o'); if ($withTools) { qa\update(); } } #[AsTask(description: 'Clears the application cache', namespace: 'app', aliases: ['cache-clear'])] function cache_clear(bool $warm = true): void { // io()->title('Clearing the application cache'); // docker_compose_run('rm -rf var/cache/'); // if ($warm) { // cache_warmup(); // } } #[AsTask(description: 'Warms the application cache', namespace: 'app', aliases: ['cache-warmup'])] function cache_warmup(): void { // io()->title('Warming the application cache'); // docker_compose_run('bin/console cache:warmup', c: context()->withAllowFailure()); } #[AsTask(description: 'Migrates database schema', namespace: 'app:db', aliases: ['migrate'])] function migrate(): void { // io()->title('Migrating the database schema'); // docker_compose_run('bin/console doctrine:database:create --if-not-exists'); // docker_compose_run('bin/console doctrine:migration:migrate -n --allow-no-migration --all-or-nothing'); } #[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['fixtures'])] function fixtures(): void { // io()->title('Loads fixtures'); // docker_compose_run('bin/console doctrine:fixture:load -n'); } ================================================ FILE: infrastructure/docker/docker-compose.dev.yml ================================================ services: router: build: services/router volumes: - "/var/run/docker.sock:/var/run/docker.sock" - "./services/router/certs:/etc/ssl/certs" ports: - "80:80" - "443:443" - "8080:8080" networks: - default profiles: - default ================================================ FILE: infrastructure/docker/docker-compose.yml ================================================ # Templates to factorize the service definitions x-templates: worker_base: &worker_base build: context: services/php target: worker user: "${USER_ID}:${USER_ID}" environment: - APP_ENV depends_on: postgres: condition: service_healthy volumes: - "../..:/var/www:cached" profiles: - worker volumes: postgres-data: {} # # Needed if $XDG_ env vars have been overridden # builder-yarn-data: {} services: postgres: image: postgres:16 environment: - POSTGRES_USER=app - POSTGRES_PASSWORD=app volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 profiles: - default frontend: build: context: services/php target: frontend cache_from: - "type=registry,ref=${REGISTRY:-}/frontend:cache" user: "${USER_ID}:${USER_ID}" environment: - APP_ENV volumes: - "../..:/var/www:cached" - "../../.home:/home/app:cached" depends_on: postgres: condition: service_healthy profiles: - default labels: - "traefik.enable=true" - "project-name=${PROJECT_NAME}" - "traefik.http.routers.${PROJECT_NAME}-frontend.rule=Host(${PROJECT_DOMAINS})" - "traefik.http.routers.${PROJECT_NAME}-frontend.tls=true" - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.rule=Host(${PROJECT_DOMAINS})" # Comment the next line to be able to access frontend via HTTP instead of HTTPS - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file" # worker_messenger: # <<: *worker_base # command: php -d memory_limit=1G /var/www/application/bin/console messenger:consume async --memory-limit=128M builder: build: context: services/php target: builder cache_from: - "type=registry,ref=${REGISTRY:-}/builder:cache" init: true user: "${USER_ID}:${USER_ID}" environment: - APP_ENV # The following list contains the common environment variables exposed by CI platforms - GITHUB_ACTIONS - CI # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari - CONTINUOUS_INTEGRATION # Travis CI, Cirrus CI - BUILD_NUMBER # Jenkins, TeamCity - RUN_ID # TaskCluster, dsari volumes: - "../..:/var/www:cached" - "../../.home:/home/app:cached" # Needed when $XDG_ env vars have overridden, to persist the yarn # cache between builder and watcher, adapt according to the location # of $XDG_DATA_HOME # - "builder-yarn-data:/data/yarn" depends_on: - postgres profiles: - builder ================================================ FILE: infrastructure/docker/services/php/Dockerfile ================================================ # hadolint global ignore=DL3008 FROM debian:12.8-slim AS php-base SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN apt-get update \ && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ gnupg \ && curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb \ && dpkg -i /tmp/debsuryorg-archive-keyring.deb \ && echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ bookworm main" > /etc/apt/sources.list.d/sury.list \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* RUN apt-get update \ && apt-get install -y --no-install-recommends \ bash-completion \ procps \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* ARG PHP_VERSION RUN apt-get update \ && apt-get install -y --no-install-recommends \ "php${PHP_VERSION}-apcu" \ "php${PHP_VERSION}-bcmath" \ "php${PHP_VERSION}-cli" \ "php${PHP_VERSION}-common" \ "php${PHP_VERSION}-curl" \ "php${PHP_VERSION}-iconv" \ "php${PHP_VERSION}-intl" \ "php${PHP_VERSION}-mbstring" \ "php${PHP_VERSION}-pgsql" \ "php${PHP_VERSION}-uuid" \ "php${PHP_VERSION}-xml" \ "php${PHP_VERSION}-zip" \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* # Configuration COPY base/php-configuration /etc/php/${PHP_VERSION} ENV PHP_VERSION=${PHP_VERSION} ENV HOME=/home/app ENV COMPOSER_MEMORY_LIMIT=-1 WORKDIR /var/www FROM php-base AS frontend RUN apt-get update \ && apt-get install -y --no-install-recommends \ nginx \ "php${PHP_VERSION}-fpm" \ runit \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \ && rm -r "/etc/php/${PHP_VERSION}/fpm/pool.d/" RUN useradd -s /bin/false nginx COPY frontend/php-configuration /etc/php/${PHP_VERSION} COPY frontend/etc/nginx/. /etc/nginx/ RUN rm -rf /etc/service/ COPY frontend/etc/service/. /etc/service/ RUN chmod 777 /etc/service/*/supervise/ RUN phpenmod app-default \ && phpenmod app-fpm EXPOSE 80 CMD ["runsvdir", "-P", "/etc/service"] FROM php-base AS worker FROM php-base AS builder SHELL ["/bin/bash", "-o", "pipefail", "-c"] ARG NODEJS_VERSION=24.x RUN curl -s https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \ && echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODEJS_VERSION} nodistro main" > /etc/apt/sources.list.d/nodesource.list # Default toys ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN apt-get update \ && apt-get install -y --no-install-recommends \ "git" \ "make" \ "nodejs" \ "php${PHP_VERSION}-dev" \ "sudo" \ "unzip" \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \ && corepack enable \ && yarn set version stable # Install a fake sudo command # This is commented out by default because it exposes a security risk if you use this image in production, but it may be useful for development # Use it at your own risk # COPY base/sudo.sh /usr/local/bin/sudo # RUN curl -L https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64 -o /usr/local/bin/gosu && \ # chmod u+s /usr/local/bin/gosu && \ # chmod +x /usr/local/bin/gosu && \ # chmod +x /usr/local/bin/sudo # Config COPY builder/php-configuration /etc/php/${PHP_VERSION} RUN phpenmod app-default \ && phpenmod app-builder # Composer COPY --from=composer/composer:2.9.5 /usr/bin/composer /usr/bin/composer # Pie RUN curl -L --output /usr/local/bin/pie https://github.com/php/pie/releases/download/1.3.9/pie.phar \ && chmod +x /usr/local/bin/pie # Autocompletion ADD https://raw.githubusercontent.com/symfony/symfony/refs/heads/7.3/src/Symfony/Component/Console/Resources/completion.bash /tmp/completion.bash # Composer symfony/console version is too old, and doest not support "API version feature", so we remove it # Hey, while we are at it, let's add some more completion RUN sed /tmp/completion.bash \ -e "s/{{ COMMAND_NAME }}/composer/g" \ -e 's/"-a{{ VERSION }}"//g' \ -e "s/{{ VERSION }}/1/g" \ > /etc/bash_completion.d/composer \ && sed /tmp/completion.bash \ -e "s/{{ COMMAND_NAME }}/console/g" \ -e "s/{{ VERSION }}/1/g" \ > /etc/bash_completion.d/console # Third party tools ENV PATH="$PATH:/var/www/tools/bin" # Good default customization RUN cat >> /etc/bash.bashrc <, docker_compose_run_environment: list, macos: bool, power_shell: bool, user_id: int, root_dir: string, registry?: ?string, } ''' ================================================ FILE: tools/php-cs-fixer/.gitignore ================================================ /vendor/ ================================================ FILE: tools/php-cs-fixer/composer.json ================================================ { "type": "project", "require": { "friendsofphp/php-cs-fixer": "^3.76.0" }, "config": { "platform": { "php": "8.3" }, "bump-after-update": true, "sort-packages": true } } ================================================ FILE: tools/phpstan/.gitignore ================================================ /var/ /vendor/ ================================================ FILE: tools/phpstan/composer.json ================================================ { "type": "project", "require": { "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.17", "phpstan/phpstan-deprecation-rules": "^2.0.3", "phpstan/phpstan-symfony": "^2.0.6" }, "config": { "allow-plugins": { "phpstan/extension-installer": true }, "bump-after-update": true, "platform": { "php": "8.3" }, "sort-packages": true } } ================================================ FILE: tools/twig-cs-fixer/.gitignore ================================================ /vendor/ ================================================ FILE: tools/twig-cs-fixer/composer.json ================================================ { "type": "project", "require": { "vincentlanglet/twig-cs-fixer": "^3.8.1" }, "config": { "platform": { "php": "8.3" }, "bump-after-update": true, "sort-packages": true } }