[
  {
    "path": ".castor/context.php",
    "content": "<?php\n\nnamespace docker;\n\nuse Castor\\Attribute\\AsContext;\nuse Castor\\Context;\nuse Symfony\\Component\\Process\\Process;\n\nuse function Castor\\log;\n\n#[AsContext(default: true)]\nfunction create_default_context(): Context\n{\n    $data = create_default_variables() + [\n        'project_name' => 'app',\n        'root_domain' => 'app.test',\n        'extra_domains' => [],\n        'php_version' => '8.5',\n        'docker_compose_files' => [\n            'docker-compose.yml',\n            'docker-compose.dev.yml',\n        ],\n        'docker_compose_run_environment' => [],\n        'macos' => false,\n        'power_shell' => false,\n        // check if posix_geteuid is available, if not, use getmyuid (windows)\n        'user_id' => \\function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(),\n        'root_dir' => \\dirname(__DIR__),\n    ];\n\n    if (file_exists($data['root_dir'] . '/infrastructure/docker/docker-compose.override.yml')) {\n        $data['docker_compose_files'][] = 'docker-compose.override.yml';\n    }\n\n    $platform = strtolower(php_uname('s'));\n    if (str_contains($platform, 'darwin')) {\n        $data['macos'] = true;\n    } elseif (\\in_array($platform, ['win32', 'win64', 'windows nt'])) {\n        $data['power_shell'] = true;\n    }\n\n    //                                                   2³² - 1\n    if (false === $data['user_id'] || $data['user_id'] > 4294967295) {\n        $data['user_id'] = 1000;\n    }\n\n    if (0 === $data['user_id']) {\n        log('Running as root? Fallback to fake user id.', 'warning');\n        $data['user_id'] = 1000;\n    }\n\n    return new Context(\n        $data,\n        pty: Process::isPtySupported(),\n        environment: [\n            'BUILDKIT_PROGRESS' => 'plain',\n        ]\n    );\n}\n\n#[AsContext(name: 'test')]\nfunction create_test_context(): Context\n{\n    $c = create_default_context();\n\n    return $c\n        ->withEnvironment([\n            'APP_ENV' => 'test',\n        ])\n    ;\n}\n\n#[AsContext(name: 'ci')]\nfunction create_ci_context(): Context\n{\n    $c = create_test_context();\n\n    return $c\n        ->withData(\n            // override the default context here\n            [\n                'docker_compose_files' => [\n                    'docker-compose.yml',\n                    // Usually, the following service is not be needed in the CI\n                    'docker-compose.dev.yml',\n                    // 'docker-compose.ci.yml',\n                ],\n            ],\n            recursive: false\n        )\n        ->withEnvironment([\n            'COMPOSE_ANSI' => 'never',\n        ])\n    ;\n}\n"
  },
  {
    "path": ".castor/database.php",
    "content": "<?php\n\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\context;\nuse function Castor\\io;\nuse function docker\\docker_compose;\n\n#[AsTask(description: 'Connect to the PostgreSQL database', name: 'db:client', aliases: ['postgres', 'pg'])]\nfunction postgres_client(): void\n{\n    io()->title('Connecting to the PostgreSQL database');\n\n    docker_compose(['exec', 'postgres', 'psql', '-U', 'app', 'app'], context()->toInteractive());\n}\n"
  },
  {
    "path": ".castor/docker.php",
    "content": "<?php\n\nnamespace docker;\n\nuse Castor\\Attribute\\AsOption;\nuse Castor\\Attribute\\AsRawTokens;\nuse Castor\\Attribute\\AsTask;\nuse Castor\\Context;\nuse Castor\\Helper\\PathHelper;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Process\\Exception\\ExceptionInterface;\nuse Symfony\\Component\\Process\\Exception\\ProcessFailedException;\nuse Symfony\\Component\\Process\\ExecutableFinder;\nuse Symfony\\Component\\Process\\Process;\nuse Symfony\\Contracts\\HttpClient\\Exception\\ExceptionInterface as HttpExceptionInterface;\n\nuse function Castor\\capture;\nuse function Castor\\context;\nuse function Castor\\finder;\nuse function Castor\\fs;\nuse function Castor\\http_client;\nuse function Castor\\io;\nuse function Castor\\open;\nuse function Castor\\run;\nuse function Castor\\variable;\n\n#[AsTask(description: 'Displays some help and available urls for the current project', namespace: '')]\nfunction about(): void\n{\n    io()->title('About this project');\n\n    io()->comment('Run <comment>castor</comment> to display all available commands.');\n    io()->comment('Run <comment>castor about</comment> to display this project help.');\n    io()->comment('Run <comment>castor help [command]</comment> to display Castor help.');\n\n    io()->section('Available URLs for this project:');\n    $urls = [variable('root_domain'), ...variable('extra_domains')];\n\n    try {\n        $routers = http_client()\n            ->request('GET', \\sprintf('http://%s:8080/api/http/routers', variable('root_domain')))\n            ->toArray()\n        ;\n        $projectName = variable('project_name');\n        foreach ($routers as $router) {\n            if (!preg_match(\"{^{$projectName}-(.*)@docker$}\", $router['name'])) {\n                continue;\n            }\n            if (\"frontend-{$projectName}\" === $router['service']) {\n                continue;\n            }\n            if (!preg_match('{^Host\\(`(?P<hosts>.*)`\\)$}', $router['rule'], $matches)) {\n                continue;\n            }\n            $hosts = explode('`) || Host(`', $matches['hosts']);\n            $urls = [...$urls, ...$hosts];\n        }\n    } catch (HttpExceptionInterface) {\n    }\n\n    io()->listing(array_map(fn ($url) => \"https://{$url}\", array_unique($urls)));\n}\n\n#[AsTask(description: 'Opens the project in your browser', namespace: '', aliases: ['open'])]\nfunction open_project(): void\n{\n    open('https://' . variable('root_domain'));\n}\n\n#[AsTask(description: 'Builds the infrastructure', aliases: ['build'])]\nfunction build(\n    #[AsOption(description: 'The service to build (default: all services)', autocomplete: 'docker\\get_service_names')]\n    ?string $service = null,\n    ?string $profile = null,\n): void {\n    generate_certificates(force: false);\n\n    io()->title('Building infrastructure');\n\n    $command = [];\n\n    $command[] = '--profile';\n    if ($profile) {\n        $command[] = $profile;\n    } else {\n        $command[] = '*';\n    }\n\n    $command = [\n        ...$command,\n        'build',\n        '--build-arg', 'PHP_VERSION=' . variable('php_version'),\n        '--build-arg', 'PROJECT_NAME=' . variable('project_name'),\n    ];\n\n    if ($service) {\n        $command[] = $service;\n    }\n\n    docker_compose($command);\n}\n\n/**\n * @param list<string> $profiles\n */\n#[AsTask(description: 'Builds and starts the infrastructure', aliases: ['up'])]\nfunction up(\n    #[AsOption(description: 'The service to start (default: all services)', autocomplete: 'docker\\get_service_names')]\n    ?string $service = null,\n    #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)]\n    array $profiles = [],\n): void {\n    if (!$service && !$profiles) {\n        io()->title('Starting infrastructure');\n    }\n\n    $command = ['up', '--detach', '--wait', '--no-build'];\n\n    if ($service) {\n        $command[] = $service;\n    }\n\n    try {\n        docker_compose($command, profiles: $profiles);\n    } catch (ExceptionInterface $e) {\n        io()->error('An error occurred while starting the infrastructure.');\n        io()->note('Did you forget to run \"castor docker:build\"?');\n        io()->note('Or you forget to login to the registry?');\n\n        throw $e;\n    }\n}\n\n/**\n * @param list<string> $profiles\n */\n#[AsTask(description: 'Stops the infrastructure', aliases: ['stop'])]\nfunction stop(\n    #[AsOption(description: 'The service to stop (default: all services)', autocomplete: 'docker\\get_service_names')]\n    ?string $service = null,\n    #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)]\n    array $profiles = [],\n): void {\n    if (!$service || !$profiles) {\n        io()->title('Stopping infrastructure');\n    }\n\n    $command = ['stop'];\n\n    if ($service) {\n        $command[] = $service;\n    }\n\n    docker_compose($command, profiles: $profiles);\n}\n\n/**\n * @param array<string> $params\n */\n#[AsTask(description: 'Opens a shell (bash) or proxy any command to the builder container', aliases: ['builder'])]\nfunction builder(#[AsRawTokens] array $params = []): int\n{\n    if (0 === \\count($params)) {\n        $params = ['bash'];\n    }\n\n    $c = context()\n        ->toInteractive()\n        ->withEnvironment($_ENV + $_SERVER)\n    ;\n\n    return (int) docker_compose_run(implode(' ', $params), c: $c)->getExitCode();\n}\n\n/**\n * @param list<string> $profiles\n */\n#[AsTask(description: 'Displays infrastructure logs', aliases: ['logs'])]\nfunction logs(\n    ?string $service = null,\n    #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)]\n    array $profiles = [],\n): void {\n    $command = ['logs', '-f', '--tail', '150'];\n\n    if ($service) {\n        $command[] = $service;\n    }\n\n    docker_compose($command, c: context()->withTty(), profiles: $profiles);\n}\n\n#[AsTask(description: 'Lists containers status', aliases: ['ps'])]\nfunction ps(bool $ports = false): void\n{\n    $command = [\n        'ps',\n        '--format', 'table {{.Name}}\\t{{.Image}}\\t{{.Status}}\\t{{.RunningFor}}\\t{{.Command}}',\n        '--no-trunc',\n    ];\n\n    if ($ports) {\n        $command[2] .= '\\t{{.Ports}}';\n    }\n\n    docker_compose($command, profiles: ['*']);\n\n    if (!$ports) {\n        io()->comment('You can use the \"--ports\" option to display ports.');\n    }\n}\n\n#[AsTask(description: 'Cleans the infrastructure (remove container, volume, networks)', aliases: ['destroy'])]\nfunction destroy(\n    #[AsOption(description: 'Force the destruction without confirmation', shortcut: 'f')]\n    bool $force = false,\n): void {\n    io()->title('Destroying infrastructure');\n\n    if (!$force) {\n        io()->warning('This will permanently remove all containers, volumes, networks... created for this project.');\n        io()->note('You can use the --force option to avoid this confirmation.');\n        if (!io()->confirm('Are you sure?', false)) {\n            io()->comment('Aborted.');\n\n            return;\n        }\n    }\n\n    docker_compose(['down', '--remove-orphans', '--volumes', '--rmi=local'], profiles: ['*']);\n    $files = finder()\n        ->in(variable('root_dir') . '/infrastructure/docker/services/router/certs/')\n        ->name('*.pem')\n        ->files()\n    ;\n    fs()->remove($files);\n}\n\n#[AsTask(description: 'Generates SSL certificates (with mkcert if available or self-signed if not)')]\nfunction generate_certificates(\n    #[AsOption(description: 'Force the certificates re-generation without confirmation', shortcut: 'f')]\n    bool $force = false,\n): void {\n    $sslDir = variable('root_dir') . '/infrastructure/docker/services/router/certs';\n\n    if (file_exists(\"{$sslDir}/cert.pem\") && !$force) {\n        io()->comment('SSL certificates already exists.');\n        io()->note('Run \"castor docker:generate-certificates --force\" to generate new certificates.');\n\n        return;\n    }\n\n    io()->title('Generating SSL certificates');\n\n    if ($force) {\n        if (file_exists($f = \"{$sslDir}/cert.pem\")) {\n            io()->comment('Removing existing certificates in infrastructure/docker/services/router/certs/*.pem.');\n            unlink($f);\n        }\n\n        if (file_exists($f = \"{$sslDir}/key.pem\")) {\n            unlink($f);\n        }\n    }\n\n    $finder = new ExecutableFinder();\n    $mkcert = $finder->find('mkcert');\n\n    if ($mkcert) {\n        $pathCaRoot = capture(['mkcert', '-CAROOT']);\n\n        if (!is_dir($pathCaRoot)) {\n            io()->warning('You must have mkcert CA Root installed on your host with \"mkcert -install\" command.');\n\n            return;\n        }\n\n        $rootDomain = variable('root_domain');\n\n        run([\n            'mkcert',\n            '-cert-file', \"{$sslDir}/cert.pem\",\n            '-key-file', \"{$sslDir}/key.pem\",\n            $rootDomain,\n            \"*.{$rootDomain}\",\n            ...variable('extra_domains'),\n        ]);\n\n        io()->success('Successfully generated SSL certificates with mkcert.');\n\n        if ($force) {\n            io()->note('Please restart the infrastructure to use the new certificates with \"castor up\" or \"castor start\".');\n        }\n\n        return;\n    }\n\n    run(['infrastructure/docker/services/router/generate-ssl.sh'], context: context()->withQuiet());\n\n    io()->success('Successfully generated self-signed SSL certificates in infrastructure/docker/services/router/certs/*.pem.');\n    io()->comment('Consider installing mkcert to generate locally trusted SSL certificates and run \"castor docker:generate-certificates --force\".');\n\n    if ($force) {\n        io()->note('Please restart the infrastructure to use the new certificates with \"castor up\" or \"castor start\".');\n    }\n}\n\n#[AsTask(description: 'Starts the workers', namespace: 'docker:worker', name: 'start', aliases: ['start-workers'])]\nfunction workers_start(): void\n{\n    io()->title('Starting workers');\n\n    $command = ['up', '--detach', '--wait', '--no-build'];\n    $profiles = ['worker', 'default'];\n\n    try {\n        docker_compose($command, profiles: $profiles);\n    } catch (ProcessFailedException $e) {\n        preg_match('/service \"(\\w+)\" depends on undefined service \"(\\w+)\"/', $e->getProcess()->getErrorOutput(), $matches);\n        if (!$matches) {\n            throw $e;\n        }\n\n        $r = new \\ReflectionFunction(__FUNCTION__);\n\n        io()->newLine();\n        io()->error('An error occurred while starting the workers.');\n        io()->warning(\\sprintf(\n            <<<'EOT'\n                The \"%1$s\" service depends on the \"%2$s\" service, which is not defined in the current docker-compose configuration.\n\n                Usually, this means that the service \"%2$s\" is not defined in the same profile (%3$s) as the \"%1$s\" service.\n\n                You can try to add its profile in the current task: %4$s:%5$s\n                EOT,\n            $matches[1],\n            $matches[2],\n            implode(', ', $profiles),\n            PathHelper::makeRelative((string) $r->getFileName()),\n            $r->getStartLine(),\n        ));\n    }\n}\n\n#[AsTask(description: 'Stops the workers', namespace: 'docker:worker', name: 'stop', aliases: ['stop-workers'])]\nfunction workers_stop(): void\n{\n    io()->title('Stopping workers');\n\n    // Docker compose cannot stop a single service in a profile, if it depends\n    // on another service in another profile. To make it work, we need to select\n    // both profiles, and so stop both services\n\n    // So we find all services, in all profiles, and manually filter the one\n    // that has the \"worker\" profile, then we stop it\n    $command = ['stop'];\n\n    foreach (get_services() as $name => $service) {\n        foreach ($service['profiles'] ?? [] as $profile) {\n            if ('worker' === $profile) {\n                $command[] = $name;\n\n                continue 2;\n            }\n        }\n    }\n\n    docker_compose($command, profiles: ['*']);\n}\n\n/**\n * @param list<string> $subCommand\n * @param list<string> $profiles\n */\nfunction docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process\n{\n    $c ??= context();\n    $profiles = $profiles ?: ['default'];\n\n    $domains = [$c['root_domain'], ...$c['extra_domains']];\n    $domains = '`' . implode('`) || Host(`', $domains) . '`';\n\n    $c = $c->withEnvironment([\n        'PROJECT_NAME' => $c['project_name'],\n        'PROJECT_ROOT_DOMAIN' => $c['root_domain'],\n        'PROJECT_DOMAINS' => $domains,\n        'USER_ID' => $c['user_id'],\n        'PHP_VERSION' => $c['php_version'],\n        'REGISTRY' => $c['registry'] ?? '',\n    ]);\n\n    if ($c['APP_ENV'] ?? false) {\n        $c = $c->withEnvironment([\n            'APP_ENV' => $c['APP_ENV'] ?? '',\n        ]);\n    }\n\n    $command = [\n        'docker',\n        'compose',\n        '-p', $c['project_name'],\n    ];\n    foreach ($profiles as $profile) {\n        $command[] = '--profile';\n        $command[] = $profile;\n    }\n\n    foreach ($c['docker_compose_files'] as $file) {\n        $command[] = '-f';\n        $command[] = $c['root_dir'] . '/infrastructure/docker/' . $file;\n    }\n\n    $command = array_merge($command, $subCommand);\n\n    return run($command, context: $c);\n}\n\nfunction docker_compose_run(\n    string $runCommand,\n    ?Context $c = null,\n    string $service = 'builder',\n    bool $noDeps = true,\n    ?string $workDir = null,\n    bool $portMapping = false,\n): Process {\n    $c ??= context();\n\n    $command = [\n        'run',\n        '--rm',\n    ];\n\n    if ($noDeps) {\n        $command[] = '--no-deps';\n    }\n\n    if ($portMapping) {\n        $command[] = '--service-ports';\n    }\n\n    if (null !== $workDir) {\n        $command[] = '-w';\n        $command[] = $workDir;\n    }\n\n    foreach ($c['docker_compose_run_environment'] as $key => $value) {\n        $command[] = '-e';\n        $command[] = \"{$key}={$value}\";\n    }\n\n    $command[] = $service;\n    $command[] = '/bin/bash';\n    $command[] = '-c';\n    $command[] = \"{$runCommand}\";\n\n    return docker_compose($command, c: $c, profiles: ['*']);\n}\n\nfunction docker_exit_code(\n    string $runCommand,\n    ?Context $c = null,\n    string $service = 'builder',\n    bool $noDeps = true,\n    ?string $workDir = null,\n): int {\n    $c = ($c ?? context())->withAllowFailure();\n\n    $process = docker_compose_run(\n        runCommand: $runCommand,\n        c: $c,\n        service: $service,\n        noDeps: $noDeps,\n        workDir: $workDir,\n    );\n\n    return $process->getExitCode() ?? 0;\n}\n\n// Mac users have a lot of problems running Yarn / Webpack on the Docker stack\n// so this func allow them to run these tools on their host\nfunction run_in_docker_or_locally_for_mac(string $command, ?Context $c = null): void\n{\n    $c ??= context();\n\n    if ($c['macos']) {\n        run($command, context: $c->withWorkingDirectory($c['root_dir']));\n    } else {\n        docker_compose_run($command, c: $c);\n    }\n}\n\n#[AsTask(description: 'Push images cache to the registry', namespace: 'docker', name: 'push', aliases: ['push'])]\nfunction push(bool $dryRun = false): void\n{\n    $registry = variable('registry');\n\n    if (!$registry) {\n        throw new \\RuntimeException('You must define a registry to push images.');\n    }\n\n    // Generate bake file\n    $targets = [];\n\n    foreach (get_services() as $service => $config) {\n        $cacheFrom = $config['build']['cache_from'][0] ?? null;\n\n        if (null === $cacheFrom) {\n            continue;\n        }\n\n        $cacheFrom = explode(',', $cacheFrom);\n        $reference = null;\n        $type = null;\n\n        if (1 === \\count($cacheFrom)) {\n            $reference = $cacheFrom[0];\n            $type = 'registry';\n        } else {\n            foreach ($cacheFrom as $part) {\n                $from = explode('=', $part);\n\n                if (2 !== \\count($from)) {\n                    continue;\n                }\n\n                if ('type' === $from[0]) {\n                    $type = $from[1];\n                }\n\n                if ('ref' === $from[0]) {\n                    $reference = $from[1];\n                }\n            }\n        }\n\n        $targets[] = [\n            'reference' => $reference,\n            'type' => $type,\n            'context' => $config['build']['context'],\n            'dockerfile' => $config['build']['dockerfile'] ?? 'Dockerfile',\n            'target' => $config['build']['target'] ?? null,\n        ];\n    }\n\n    $content = \\sprintf(<<<'EOHCL'\n        group \"default\" {\n            targets = [%s]\n        }\n\n        EOHCL\n        , implode(', ', array_map(fn ($target) => \\sprintf('\"%s\"', $target['target']), $targets)));\n\n    foreach ($targets as $target) {\n        $content .= \\sprintf(<<<'EOHCL'\n            target \"%s\" {\n                context    = \"%s\"\n                dockerfile = \"%s\"\n                cache-from = [\"%s\"]\n                cache-to   = [\"type=%s,ref=%s,mode=max\"]\n                target     = \"%s\"\n                args = {\n                    PHP_VERSION = \"%s\"\n                }\n            }\n\n            EOHCL\n            , $target['target'], $target['context'], $target['dockerfile'], $target['reference'], $target['type'], $target['reference'], $target['target'], variable('php_version'));\n    }\n\n    if ($dryRun) {\n        io()->write($content);\n\n        return;\n    }\n\n    // write bake file in tmp file\n    $bakeFile = tempnam(sys_get_temp_dir(), 'bake');\n    file_put_contents($bakeFile, $content);\n\n    // Run bake\n    run(['docker', 'buildx', 'bake', '-f', $bakeFile]);\n}\n\n/**\n * @return array<string, array{profiles?: list<string>, build: array{context: string, dockerfile?: string, cache_from?: list<string>, target?: string}}>\n */\nfunction get_services(): array\n{\n    return json_decode(\n        docker_compose(\n            ['config', '--format', 'json'],\n            context()->withQuiet(),\n            profiles: ['*'],\n        )->getOutput(),\n        true,\n    )['services'];\n}\n\n/**\n * @return string[]\n */\nfunction get_service_names(): array\n{\n    return array_keys(get_services());\n}\n"
  },
  {
    "path": ".castor/init.php",
    "content": "<?php\n\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\fs;\nuse function Castor\\variable;\nuse function docker\\build;\nuse function docker\\docker_compose_run;\n\n#[AsTask(description: 'Initialize the project')]\nfunction init(): void\n{\n    fs()->remove([\n        '.github/',\n        'README.md',\n        'CHANGELOG.md',\n        'CONTRIBUTING.md',\n        'LICENSE',\n        __FILE__,\n    ]);\n    fs()->rename('README.dist.md', 'README.md');\n\n    $readMeContent = file_get_contents('README.md');\n\n    if (false === $readMeContent) {\n        return;\n    }\n\n    $urls = [variable('root_domain'), ...variable('extra_domains')];\n    $readMeContent = str_replace('<your hostnames>', implode(' ', $urls), $readMeContent);\n    file_put_contents('README.md', $readMeContent);\n}\n\n#[AsTask(description: 'Install Symfony')]\nfunction symfony(bool $webApp = false): void\n{\n    $base = rtrim(variable('root_dir') . '/application');\n\n    $gitIgnore = $base . '/.gitignore';\n    $gitIgnoreContent = '';\n    if (file_exists($gitIgnore)) {\n        $gitIgnoreContent = file_get_contents($gitIgnore);\n    }\n\n    build();\n    docker_compose_run('composer create-project symfony/skeleton sf');\n\n    fs()->mirror($base . '/sf/', $base);\n    fs()->remove([$base . '/sf', $base . '/var']);\n\n    if ($webApp) {\n        docker_compose_run('composer require webapp');\n    }\n\n    docker_compose_run(\"sed -i 's#^DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\\\\?serverVersion=16\\\\&charset=utf8#' .env\");\n    file_put_contents($gitIgnore, $gitIgnoreContent, \\FILE_APPEND);\n}\n"
  },
  {
    "path": ".castor/qa.php",
    "content": "<?php\n\nnamespace qa;\n\n// use Castor\\Attribute\\AsRawTokens;\nuse Castor\\Attribute\\AsOption;\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\io;\nuse function Castor\\variable;\nuse function docker\\docker_compose_run;\nuse function docker\\docker_exit_code;\n\n#[AsTask(description: 'Runs all QA tasks')]\nfunction all(): int\n{\n    $cs = cs();\n    $phpstan = phpstan();\n    $twigCs = twigCs();\n    // $phpunit = phpunit();\n\n    return max($cs, $phpstan, $twigCs/* , $phpunit */);\n}\n\n#[AsTask(description: 'Installs tooling')]\nfunction install(): void\n{\n    io()->title('Installing QA tooling');\n\n    docker_compose_run('composer install -o', workDir: '/var/www/tools/php-cs-fixer');\n    docker_compose_run('composer install -o', workDir: '/var/www/tools/phpstan');\n    docker_compose_run('composer install -o', workDir: '/var/www/tools/twig-cs-fixer');\n}\n\n#[AsTask(description: 'Updates tooling')]\nfunction update(): void\n{\n    io()->title('Updating QA tooling');\n\n    docker_compose_run('composer update -o', workDir: '/var/www/tools/php-cs-fixer');\n    docker_compose_run('composer update -o', workDir: '/var/www/tools/phpstan');\n    docker_compose_run('composer update -o', workDir: '/var/www/tools/twig-cs-fixer');\n}\n\n// /**\n//  * @param string[] $rawTokens\n//  */\n// #[AsTask(description: 'Runs PHPUnit', aliases: ['phpunit'])]\n// function phpunit(#[AsRawTokens] array $rawTokens = []): int\n// {\n//     io()->section('Running PHPUnit...');\n//\n//     return docker_exit_code('bin/phpunit ' . implode(' ', $rawTokens));\n// }\n\n#[AsTask(description: 'Runs PHPStan', aliases: ['phpstan'])]\nfunction phpstan(\n    #[AsOption(description: 'Generate baseline file', shortcut: 'b')]\n    bool $baseline = false,\n): int {\n    if (!is_dir(variable('root_dir') . '/tools/phpstan/vendor')) {\n        install();\n    }\n\n    io()->section('Running PHPStan...');\n\n    $options = $baseline ? '--generate-baseline --allow-empty-baseline' : '';\n    $command = \\sprintf('phpstan analyse --memory-limit=-1 %s -v', $options);\n\n    return docker_exit_code($command, workDir: '/var/www');\n}\n\n#[AsTask(description: 'Fixes Coding Style', aliases: ['cs'])]\nfunction cs(bool $dryRun = false): int\n{\n    if (!is_dir(variable('root_dir') . '/tools/php-cs-fixer/vendor')) {\n        install();\n    }\n\n    io()->section('Running PHP CS Fixer...');\n\n    if ($dryRun) {\n        return docker_exit_code('php-cs-fixer fix --dry-run --diff', workDir: '/var/www');\n    }\n\n    return docker_exit_code('php-cs-fixer fix', workDir: '/var/www');\n}\n\n#[AsTask(description: 'Fixes Twig Coding Style', aliases: ['twig-cs'])]\nfunction twigCs(bool $dryRun = false): int\n{\n    if (!is_dir(variable('root_dir') . '/tools/twig-cs-fixer/vendor')) {\n        install();\n    }\n\n    io()->section('Running Twig CS Fixer...');\n\n    if ($dryRun) {\n        return docker_exit_code('twig-cs-fixer', workDir: '/var/www');\n    }\n\n    return docker_exit_code('twig-cs-fixer --fix', workDir: '/var/www');\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Force LF line ending (mandatory for Windows)\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/workflows/cache.yml",
    "content": "name: Push docker image to registry\n\n\"on\":\n  push:\n    # Only run this job when pushing to the main branch\n    branches: [\"main\"]\n\npermissions:\n  contents: read\n  packages: write\n\nenv:\n  DS_REGISTRY: \"ghcr.io/jolicode/docker-starter\"\n  DS_PHP_VERSION: \"8.5\"\n\njobs:\n  push-images:\n    name: Push image to registry\n    runs-on: ubuntu-latest\n    steps:\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n\n      - name: Log in to registry\n        shell: bash\n        run: echo \"${{ secrets.GITHUB_TOKEN }}\" | docker login ghcr.io -u $ --password-stdin\n\n      - name: setup-castor\n        uses: castor-php/setup-castor@v1.0.0\n\n      - uses: actions/checkout@v6\n\n\n      - name: \"Build and start the infrastructure\"\n        run: \"castor docker:push\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Continuous Integration\n\n\"on\":\n    push:\n        branches: [\"main\"]\n    pull_request:\n        branches: [\"main\"]\n    schedule:\n        - cron: \"0 0 * * MON\"\n\npermissions:\n    contents: read\n    packages: read\n\nenv:\n    # Fix for symfony/color detection. We know GitHub Actions can handle it\n    ANSICON: 1\n    CASTOR_CONTEXT: ci\n    DS_REGISTRY: \"ghcr.io/jolicode/docker-starter\"\n\njobs:\n    check-dockerfiles:\n        name: Check Dockerfile\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout\n              uses: actions/checkout@v6\n\n            - name: Check php/Dockerfile\n              uses: hadolint/hadolint-action@v3.3.0\n              with:\n                  dockerfile: infrastructure/docker/services/php/Dockerfile\n\n    ci:\n        name: Test with PHP ${{ matrix.php-version }}\n        strategy:\n            fail-fast: false\n            matrix:\n                php-version: [\"8.3\", \"8.4\", \"8.5\"]\n        runs-on: ubuntu-latest\n        env:\n            DS_PHP_VERSION: ${{ matrix.php-version }}\n        steps:\n            - name: Set up Docker Buildx\n              uses: docker/setup-buildx-action@v4\n\n            - name: Log in to registry\n              shell: bash\n              run: echo \"${{ secrets.GITHUB_TOKEN }}\" | docker login ghcr.io -u $ --password-stdin\n\n            - name: setup-castor\n              uses: castor-php/setup-castor@v1.0.0\n\n            - uses: actions/checkout@v6\n\n            - name: \"Build and start the infrastructure\"\n              run: \"castor start\"\n\n            - name: \"Check PHP coding standards\"\n              run: \"castor qa:cs --dry-run\"\n\n            - name: \"Run PHPStan\"\n              run: \"castor qa:phpstan\"\n\n            - name: \"Test HTTP server\"\n              run: |\n                  set -e\n                  set -o pipefail\n\n                  curl --fail --insecure --silent -H \"Host: app.test\" https://127.0.0.1 | grep \"Hello world\"\n                  curl --fail --insecure --silent -H \"Host: app.test\" https://127.0.0.1 | grep \"${{ matrix.php-version }}\"\n\n            - name: \"Test builder\"\n              run: |\n                  set -e\n                  set -o pipefail\n\n                  cat > .castor/test.php <<'EOPHP'\n                  <?php\n\n                  use Castor\\Attribute\\AsTask;\n                  use function docker\\docker_compose_run;\n\n                  #[AsTask()]\n                  function test()\n                  {\n                      docker_compose_run('echo \"Hello World\"');\n                  }\n\n                  #[AsTask()]\n                  function app_env()\n                  {\n                      docker_compose_run('php public/index.php');\n                  }\n                  EOPHP\n\n                  castor test | grep \"Hello World\"\n                  CASTOR_CONTEXT=default castor app-env | grep 'Environment: not set'\"\n                  CASTOR_CONTEXT=test castor app-env | grep 'Environment: test'\"\n\n            - name: \"Test communication with DB\"\n              run: |\n                  set -e\n                  set -o pipefail\n\n                  cat > application/public/index.php <<'EOPHP'\n                  <?php\n                  $pdo = new PDO('pgsql:host=postgres;dbname=app', 'app', 'app');\n                  $pdo->exec('CREATE TABLE test (id integer NOT NULL)');\n                  $pdo->exec('INSERT INTO test VALUES (1)');\n                  echo $pdo->query('SELECT * from test')->fetchAll() ? 'database OK' : 'database KO';\n                  EOPHP\n\n                  # FPM seems super slow to detect the change, we need to wait a bit\n                  sleep 3\n\n                  curl --fail --insecure --silent -H \"Host: app.test\" https://127.0.0.1 | grep \"database OK\"\n"
  },
  {
    "path": ".gitignore",
    "content": "/.castor.stub.php\n/infrastructure/docker/docker-compose.override.yml\n/infrastructure/docker/services/router/certs/*.pem\n\n# Tools\n.php-cs-fixer.cache\n.twig-cs-fixer.cache\n"
  },
  {
    "path": ".home/.gitignore",
    "content": "/*\n!.gitignore\n"
  },
  {
    "path": ".php-cs-fixer.php",
    "content": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->ignoreVCSIgnored(true)\n    ->ignoreDotFiles(false)\n    ->in(__DIR__)\n    ->append([\n        __FILE__,\n    ])\n;\n\nreturn (new PhpCsFixer\\Config())\n    ->setUnsupportedPhpVersionAllowed(true)\n    ->setRiskyAllowed(true)\n    ->setRules([\n        '@PHP83Migration' => true,\n        '@PhpCsFixer' => true,\n        '@Symfony' => true,\n        '@Symfony:risky' => true,\n        'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it\n        'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it\n        'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it\n        'concat_space' => ['spacing' => 'one'],\n        'ordered_class_elements' => true, // Symfony(PSR12) override the default value, but we don't want\n        'blank_line_before_statement' => true, // Symfony(PSR12) override the default value, but we don't want\n    ])\n    ->setFinder($finder)\n;\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n\n## 4.0.0 (not released yet)\n\n* Tooling\n  * Migrate from Invoke to Castor\n  * Add `castor symfony` to install a Symfony application\n  * Add `castor init` command to initialize a new project\n  * Add a `test` context\n* Services\n  * Upgrade Traefik from v2.7 to v3.0\n  * Upgrade to PostgreSQL v16\n  * Replace maildev with Mailpit\n* PHP\n  * Drop support for PHP < 8.3\n  * Add support for PHP 8.4, 8.5\n  * Add support for pie\n  * Add some PHP tooling (PHP-CS-Fixer, PHPStan, Twig-CS-Fixer)\n  * Update Composer to version 2.8\n* Builder\n  * Update NodeJS to version 20.x LTS\n  * Add support for autocomplete (composer & symfony)\n* Docker:\n  * Add a dockerfile linter\n  * Do not store certificates in the router image\n  * Do not hardcode a user in the Dockerfile (and so the image), map it dynamically\n  * Mount the project in `/var/www` instead of `/home/app`\n  * Add support for caching image cache in a registry\n  * Upgrade base to Debian Bookworm (12.5)\n\n## 3.11.0 (2023-05-30)\n\n* Use docker stages to build images\n\n## 3.10.0 (2023-05-22)\n\n* Fix workers detection in docker v23\n* Update Invoke to version 2.1\n* Update Composer to version 2.5.5\n* Upgrade NodeJS to 18.x LTS version\n* Migrate to Compose V2\n\n## 3.9.0 (2022-12-21)\n\n* Update documentation cookbook for installing redirection.io and Blackfire to\n  remove deprecated apt-key usage\n* Update Composer to version 2.5.0\n* Increase the number of FPM worker from 4 to 25\n* Enabled PHP FPM status page on `/php-fpm-status`\n* Added support for PHP 8.2\n\n## 3.8.0 (2022-06-15)\n\n* Add documentation cookbook for using pg_activity\n* Forward CI env vars in Docker containers\n* Run the npm/yarn/webpack commands on the host for all mac users (even the ones not using Dinghy)\n* Tests with PHP 7.4, 8.0, and 8.1\n\n## 3.7.0 (2022-05-24)\n\n* Add documentation cookbook for installing redirection.io\n* Upgrade to Traefik v2.7.0\n* Upgrade to PostgreSQL v14\n* Upgrade to Composer v2.3\n\n## 3.6.0 (2022-03-10)\n\n* Upgrade NodeJS to version 16.x LTS and remove deprecated apt-key usage\n* Various fix in the documentation\n* Remove certificates when destroying infrastructure\n\n## 3.5.0 (2022-01-27)\n\n* Update PHP to version 8.1\n* Generate SSL certificates with mkcert when available (self-signed otherwise)\n\n## 3.4.0 (2021-10-13)\n\n* Fix `COMPOSER_CACHE_DIR` default value when composer is not installed on host\n* Upgrade base to Debian Bullseye (11.0)\n* Document webpack 5+ integration\n\n## 3.3.0 (2021-06-03)\n\n* Update PHP to version 8.0\n* Update Composer to version 2.1.0\n* Fix APT key for Sury repository\n* Fix the version of our debian base image\n\n## 3.2.0 (2021-02-17)\n\n* Migrate CI from Circle to GitHub Actions\n* Add support for `docker-compose.override.yml`\n\n## 3.1.0 (2020-11-13)\n\n * Fix TTY on all OS\n * Add default vendor installation command with auto-detection in `install()` for Yarn, NPM and Composer\n * Update Composer to version 2\n * Install by default php-uuid extension\n * Update NodeJS from 12.x to 14.x\n\n## 3.0.0 (2020-07-01)\n\n * Migrate from Fabric to Invoke\n * Migrate from Alpine to Debian for PHP images\n * Add a confirmation when calling `inv destroy`\n * Tweak the PHP configuration\n * Upgrade PostgreSQL from 11 to 12\n * Upgrade Traefik from 2.0 to 2.2\n * Add an `help` task. This is the default one\n * The help command list all HTTP(s) services available\n * The help command list tasks available\n * Fix the support for Mac and Windows\n * Try to map the correct Composer cache dir from the host\n * Enhance the documentation\n\n## 2.0.0 (2020-01-08)\n\n* Better Docker for Windows support\n* Add support for running many projects at the same time\n* Upgrade Traefik from 1.7 to 2.0\n* Add native support for workers\n\n## 1.0.0 (2019-07-27)\n\n* First release\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nFirst of all, **thank you** for contributing, **you are awesome**!\n\nEverybody should be able to help. Here's how you can do it:\n\n1. [Fork it](https://github.com/jolicode/docker-starter/fork_select)\n2. improve it\n3. submit a [pull request](https://help.github.com/articles/creating-a-pull-request)\n\nHere's some tips to make you the best contributor ever:\n\n* [Rules](#rules)\n* [Keeping your fork up-to-date](#keeping-your-fork-up-to-date)\n\n## Rules\n\nHere are a few rules to follow in order to ease code reviews, and discussions\nbefore maintainers accept and merge your work.\n\nPlease, write [commit messages that make\nsense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),\nand [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)\nbefore submitting your Pull Request (see also how to [keep your\nfork up-to-date](#keeping-your-fork-up-to-date)).\n\nOne may ask you to [squash your\ncommits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)\ntoo. This is used to \"clean\" your Pull Request before merging it (we don't want\ncommits such as `fix tests`, `fix 2`, `fix 3`, etc.).\n\nAlso, while creating your Pull Request on GitHub, you MUST write a description\nwhich gives the context and/or explains why you are creating it.\n\nYour work will then be reviewed as soon as possible (suggestions about some\nchanges, improvements or alternatives may be given).\n\n## Keeping your fork up-to-date\n\nTo keep your fork up-to-date, you should track the upstream (original) one\nusing the following command:\n\n\n```shell\ngit remote add upstream https://github.com/jolicode/docker-starter.git\n```\n\nThen get the upstream changes:\n\n```shell\ngit checkout master\ngit pull --rebase upstream master\ngit checkout <your-branch>\ngit rebase master\n```\n\nFinally, publish your changes:\n\n```shell\ngit push -f origin <your-branch>\n```\n\nYour pull request will be automatically updated.\n\nThank you!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019-present JoliCode\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.dist.md",
    "content": "# My project\n\n## Running the application locally\n\n### Requirements\n\nA Docker environment is provided and requires you to have these tools available:\n\n * Docker\n * Bash\n * [Castor](https://github.com/jolicode/castor#installation)\n\n#### Castor\n\nOnce `castor` is installed, in order to improve your usage of `castor` scripts, you\ncan install console autocompletion script.\n\nIf you are using bash:\n\n```bash\ncastor completion | sudo tee /etc/bash_completion.d/castor\n```\n\nIf you are using something else, please refer to your shell documentation. You\nmay need to use `castor completion > /to/somewhere`.\n\n`castor` supports completion for `bash`, `zsh` & `fish` shells.\n\n### Docker environment\n\nThe Docker infrastructure provides a web stack with:\n - NGINX\n - PostgreSQL\n - PHP\n - Traefik\n - A container with some tooling:\n   - Composer\n   - Node\n   - Yarn / NPM\n\n### Domain configuration (first time only)\n\nBefore running the application for the first time, ensure your domain names\npoint the IP of your Docker daemon by editing your `/etc/hosts` file.\n\nThis IP is probably `127.0.0.1` unless you run Docker in a special VM (like docker-machine for example).\n\n> [!NOTE]\n> The router binds port 80 and 443, that's why it will work with `127.0.0.1`\n\n```\necho '127.0.0.1 <your hostnames>' | sudo tee -a /etc/hosts\n```\n\n### Starting the stack\n\nLaunch the stack by running this command:\n\n```bash\ncastor start\n```\n\n> [!NOTE]\n> the first start of the stack should take a few minutes.\n\nThe site is now accessible at the hostnames you have configured over HTTPS\n(you may need to accept self-signed SSL certificate if you do not have `mkcert`\ninstalled on your computer - see below).\n\n### SSL certificates\n\nHTTPS is supported out of the box. SSL certificates are not versioned and will\nbe generated the first time you start the infrastructure (`castor start`) or if\nyou run `castor docker:generate-certificates`.\n\nIf you have `mkcert` installed on your computer, it will be used to generate\nlocally trusted certificates. See [`mkcert` documentation](https://github.com/FiloSottile/mkcert#installation)\nto understand how to install it. Do not forget to install CA root from `mkcert`\nby running `mkcert -install`.\n\nIf you don't have `mkcert`, then self-signed certificates will instead be\ngenerated with `openssl`. You can configure [infrastructure/docker/services/router/openssl.cnf](infrastructure/docker/services/router/openssl.cnf)\nto tweak certificates.\n\nYou can run `castor docker:generate-certificates --force` to recreate new certificates\nif some were already generated. Remember to restart the infrastructure to make\nuse of the new certificates with `castor build && castor up` or `castor start`.\n\n### Builder\n\nHaving some composer, yarn or other modifications to make on the project?\nStart the builder which will give you access to a container with all these\ntools available:\n\n```bash\ncastor builder\n```\n\n### Other tasks\n\nCheckout `castor` to have the list of available tasks.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n  <a href=\"https://github.com/jolicode/docker-starter\"><img src=\"https://jolicode.com/media/original/oss/headers/docker-starter.png\" alt=\"Docker Starter\"></a>\n  <br />\n  Docker Starter\n  <br />\n  <sub><em><h6>A docker-based infrastructure wrapped in an easy-to-use command line, oriented for PHP projects.</h6></em></sub>\n</h1>\n\nThis repository contains a collection of Dockerfile and docker-compose configurations\nfor your PHP projects with built-in support for HTTPS, custom domain, databases, workers...\nand is used as a foundation for our projects at [JoliCode](https://jolicode.com/).\n\n> [!WARNING]\n> You are reading the README of version 4 that uses [Castor](https://castor.jolicode.com).\n\n* 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);\n* 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);\n\n## Project configuration\n\nBefore executing any command, you need to configure a few parameters in the\n`castor.php` file, in the `create_default_variables()` function:\n\n* `project_name` (**required**): This will be used to prefix all docker objects\n(network, images, containers);\n\n* `root_domain` (optional, default: `project_name + '.test'`): This is the root\ndomain where the application will be available;\n\n* `extra_domains` (optional): This contains extra domains where the application\nwill be available;\n\n* `php_version` (optional, default: `8.5`): This is PHP version.\n\nFor example:\n\n```php\nfunction create_default_variables(): array\n{\n    $projectName = 'app';\n    $tld = 'test';\n\n    return [\n        'project_name' => $projectName,\n        'root_domain' => \"{$projectName}.{$tld}\",\n        'extra_domains' => [\n            \"www.{$projectName}.{$tld}\",\n            \"admin.{$projectName}.{$tld}\",\n            \"api.{$projectName}.{$tld}\",\n        ],\n        'php_version' => 8.3,\n    ];\n)\n```\n\nWill give you `https://app.test`,  `https://www.app.test`,\n`https://api.app.test` and `https://admin.app.test` pointing at your\n`application/` directory.\n\n> [!NOTE]\n> Some castor tasks have been added for DX purposes. Checkout and adapt\n> the tasks `install`, `migrate` and `cache_clear` to your project.\n\n## Usage documentation\n\nWe provide a [README.dist.md](./README.dist.md) to bootstrap your project\ndocumentation, with everything you need to know to start and interact with the\ninfrastructure.\n\nIf you want to install a Symfony project, you can run (before `castor init`):\n\n```\ncastor symfony [--web-app]\n```\n\nTo replace this README with the dist, and remove all unnecessary files, you can\nrun:\n\n```bash\ncastor init\n```\n\n> [!NOTE]\n> This command can be run only once\n\nAlso, in order to improve your usage of castor scripts, you can install console\nautocompletion script.\n\nIf you are using bash:\n\n```bash\ncastor completion | sudo tee /etc/bash_completion.d/castor\n```\n\nIf you are using something else, please refer to your shell documentation. You\nmay need to use `castor completion > /to/somewhere`.\n\nCastor supports completion for `bash`, `zsh` & `fish` shells.\n\n## Cookbooks\n\n### How to install third party tools with Composer\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIf you want to install some third party tools with Composer, it is recommended to install them in their dedicated directory.\nPHPStan and PHP-CS-Fixer are already installed in the `tools` directory.\n\nWe suggest to:\n\n1. create a composer.json which requires only this tool in `tools/<tool name>/composer.json`;\n\n1. create an executable symbolic link to the tool from the root directory of the project: `ln -s ../<tool name>/vendor/bin/<tool bin> tools/bin/<tool bin>`;\n\n> [!NOTE]\n> Relative symlinks works here, because the first part of the command is relative to the second part, not to the current directory.\n\nSince `tools/bin` path is appended to the `$PATH`, tools will be available globally in the builder container.\n\n</details>\n\n### How to change the layout of the project\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIf you want to rename the `application` directory, or even move its content to\nthe root directory, you have to edit each reference to it. Theses references\nrepresent each application entry point, whether it be over HTTP or CLI.\nUsually, there is three places where you need to do it:\n\n* In Nginx configuration file:\n  `infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf`. You need\n  to update  `http.server.root` option to the new path. For example:\n  ```diff\n  - root /var/www/application/public;\n  + root /var/www/public;\n  ```\n* In all workers configuration file:\n  `infrastructure/docker/docker-compose.worker.yml`:\n  ```diff\n  - command: php -d memory_limit=1G /var/www/application/bin/console messenger:consume async --memory-limit=128M\n  + command: php -d memory_limit=1G /var/www/bin/console messenger:consume async --memory-limit=128M\n  ```\n* In the builder, to land in the right directory directly:\n  `infrastructure/docker/services/php/Dockerfile`:\n  ```diff\n  - WORKDIR /var/www/application\n  + WORKDIR /var/www\n  ```\n\n</details>\n\n### How to use MariaDB instead of PostgreSQL\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use MariaDB, you will need to apply this patch:\n\n```diff\ndiff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml\nindex d00f315..bdfdc65 100644\n--- a/infrastructure/docker/docker-compose.builder.yml\n+++ b/infrastructure/docker/docker-compose.builder.yml\n@@ -10,7 +10,7 @@ services:\n     builder:\n         build: services/builder\n         depends_on:\n-            - postgres\n+            - mariadb\n         environment:\n             - COMPOSER_MEMORY_LIMIT=-1\n         volumes:\ndiff --git a/infrastructure/docker/docker-compose.worker.yml b/infrastructure/docker/docker-compose.worker.yml\nindex 2eda814..59f8fed 100644\n--- a/infrastructure/docker/docker-compose.worker.yml\n+++ b/infrastructure/docker/docker-compose.worker.yml\n@@ -5,7 +5,7 @@ x-services-templates:\n     worker_base: &worker_base\n         build: services/worker\n         depends_on:\n-            - postgres\n+            - mariadb\n             #- rabbitmq\n         volumes:\n             - \"../..:/var/www:cached\"\ndiff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml\nindex 49a2661..1804a01 100644\n--- a/infrastructure/docker/docker-compose.yml\n+++ b/infrastructure/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: '3.7'\n\n volumes:\n-    postgres-data: {}\n+    mariadb-data: {}\n\n services:\n     router:\n@@ -13,7 +13,7 @@ services:\n     frontend:\n         build: services/frontend\n         depends_on:\n-            - postgres\n+            - mariadb\n         volumes:\n             - \"../..:/var/www:cached\"\n         labels:\n@@ -24,10 +24,7 @@ services:\n             # Comment the next line to be able to access frontend via HTTP instead of HTTPS\n             - \"traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file\"\n\n-    postgres:\n-        image: postgres:16\n-        environment:\n-            - POSTGRES_USER=app\n-            - POSTGRES_PASSWORD=app\n+    mariadb:\n+        image: mariadb:11\n+        environment:\n+            - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1\n+        healthcheck:\n+            test: \"mariadb-admin ping -h localhost\"\n+            interval: 5s\n+            timeout: 5s\n+            retries: 10\n         volumes:\n-            - postgres-data:/var/lib/postgresql/data\n+            - mariadb-data:/var/lib/mysql\ndiff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile\nindex 56e1835..95fee78 100644\n--- a/infrastructure/docker/services/php/Dockerfile\n+++ b/infrastructure/docker/services/php/Dockerfile\n@@ -24,7 +24,7 @@ RUN apk add --no-cache \\\n     php${PHP_VERSION}-intl \\\n     php${PHP_VERSION}-mbstring \\\n-    php${PHP_VERSION}-pgsql \\\n+    php${PHP_VERSION}-mysql \\\n     php${PHP_VERSION}-xml \\\n     php${PHP_VERSION}-zip \\\n```\n\n</details>\n\n### How to use MySQL instead of PostgreSQL\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use MySQL, you will need to apply this patch:\n\n```diff\ndiff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml\nindex d00f315..bdfdc65 100644\n--- a/infrastructure/docker/docker-compose.builder.yml\n+++ b/infrastructure/docker/docker-compose.builder.yml\n@@ -10,7 +10,7 @@ services:\n     builder:\n         build: services/builder\n         depends_on:\n-            - postgres\n+            - mysql\n         environment:\n             - COMPOSER_MEMORY_LIMIT=-1\n         volumes:\ndiff --git a/infrastructure/docker/docker-compose.worker.yml b/infrastructure/docker/docker-compose.worker.yml\nindex 2eda814..59f8fed 100644\n--- a/infrastructure/docker/docker-compose.worker.yml\n+++ b/infrastructure/docker/docker-compose.worker.yml\n@@ -5,7 +5,7 @@ x-services-templates:\n     worker_base: &worker_base\n         build: services/worker\n         depends_on:\n-            - postgres\n+            - mysql\n             #- rabbitmq\n         volumes:\n             - \"../..:/var/www:cached\"\ndiff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml\nindex 49a2661..1804a01 100644\n--- a/infrastructure/docker/docker-compose.yml\n+++ b/infrastructure/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: '3.7'\n\n volumes:\n-    postgres-data: {}\n+    mysql-data: {}\n\n services:\n     router:\n@@ -13,7 +13,7 @@ services:\n     frontend:\n         build: services/frontend\n         depends_on:\n-            - postgres\n+            - mysql\n         volumes:\n             - \"../..:/var/www:cached\"\n         labels:\n@@ -24,10 +24,7 @@ services:\n             # Comment the next line to be able to access frontend via HTTP instead of HTTPS\n             - \"traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file\"\n\n-    postgres:\n-        image: postgres:16\n-        environment:\n-            - POSTGRES_USER=app\n-            - POSTGRES_PASSWORD=app\n+    mysql:\n+        image: mysql:8\n+        environment:\n+            - MYSQL_ALLOW_EMPTY_PASSWORD=1\n+        healthcheck:\n+            test: \"mysqladmin ping -h localhost\"\n+            interval: 5s\n+            timeout: 5s\n+            retries: 10\n         volumes:\n-            - postgres-data:/var/lib/postgresql/data\n+            - mysql-data:/var/lib/mysql\ndiff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile\nindex 56e1835..95fee78 100644\n--- a/infrastructure/docker/services/php/Dockerfile\n+++ b/infrastructure/docker/services/php/Dockerfile\n@@ -24,7 +24,7 @@ RUN apk add --no-cache \\\n     php${PHP_VERSION}-intl \\\n     php${PHP_VERSION}-mbstring \\\n-    php${PHP_VERSION}-pgsql \\\n+    php${PHP_VERSION}-mysql \\\n     php${PHP_VERSION}-xml \\\n     php${PHP_VERSION}-zip \\\n```\n\n</details>\n\n### How to use with Webpack Encore\n\n<details>\n\n<summary>Read the cookbook</summary>\n\n> [!NOTE]\n> this cookbook documents the integration of webpack 5+. For older version\n> of webpack, use previous version of the docker starter.\n\nIf you want to use Webpack Encore in a Symfony project,\n\n1. Follow [instructions on symfony.com](https://symfony.com/doc/current/frontend/encore/installation.html#installing-encore-in-symfony-applications) to install webpack encore.\n\n    You will need to follow [these instructions](https://symfony.com/doc/current/frontend/encore/simple-example.html) too.\n\n2. Create a new service for encore:\n\n    Add the following content to the `docker-compose.yml` file:\n\n    ```yaml\n    services:\n        encore:\n            build:\n                context: services/php\n                target: builder\n            volumes:\n                - \"../..:/var/www:cached\"\n            command: >\n                yarn run dev-server\n                    --hot\n                    --host 0.0.0.0\n                    --public https://encore.${PROJECT_ROOT_DOMAIN}\n                    --allowed-hosts ${PROJECT_ROOT_DOMAIN}\n                    --allowed-hosts encore.${PROJECT_ROOT_DOMAIN}\n                    --client-web-socket-url-hostname encore.${PROJECT_ROOT_DOMAIN}\n                    --client-web-socket-url-port 443\n                    --client-web-socket-url-protocol wss\n                    --server-type http\n            labels:\n                - \"project-name=${PROJECT_NAME}\"\n                - \"traefik.enable=true\"\n                - \"traefik.http.routers.${PROJECT_NAME}-encore.rule=Host(`encore.${PROJECT_ROOT_DOMAIN}`)\"\n                - \"traefik.http.routers.${PROJECT_NAME}-encore.tls=true\"\n                - \"traefik.http.services.encore.loadbalancer.server.port=8000\"\n            healthcheck:\n            test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/build/app.css\"]\n            profiles:\n                - default\n    ```\n\nIf the assets are not reachable, you may accept self-signed certificate. To do so, open a new tab\nat https://encore.app.test and click on accept.\n\n</details>\n\n### How to use with AssetMapper\n\n<details>\n\n<summary>Read the cookbook</summary>\n\n1. Follow [instructions on symfony.com](https://symfony.com/doc/current/frontend/asset_mapper.html#installation) to install AssetMapper.\n\n1. Remove this block in the\n`infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` file:\n\n    ```\n    location ~* \\.(jpg|jpeg|png|gif|ico|css|js|svg)$ {\n        access_log off;\n        add_header Cache-Control \"no-cache\";\n    }\n    ```\n\n1. Remove these lines in the `infrastructure/docker/services/php/Dockerfile` file:\n\n    ```diff\n    SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n    - ARG NODEJS_VERSION=18.x\n    - RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor > /usr/share/keyrings/nodesource.gpg \\\n    -     && 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\n\n    # Default toys\n    RUN apt-get update \\\n        && apt-get install -y --no-install-recommends \\\n            git \\\n            make \\\n    -       nodejs \\\n            sudo \\\n            unzip \\\n        && apt-get clean \\\n    -   && npm install -g yarn@1.22 \\\n        && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n    ```\n</details>\n\n### How to add Elasticsearch and Kibana\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use Elasticsearch and Kibana, you should add the following content\nto the `docker-compose.yml` file:\n\n```yaml\nvolumes:\n    elasticsearch-data: {}\n\nservices:\n    elasticsearch:\n        image: elasticsearch:7.8.0\n        volumes:\n            - elasticsearch-data:/usr/share/elasticsearch/data\n        environment:\n            - \"discovery.type=single-node\"\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-elasticsearch.rule=Host(`elasticsearch.${PROJECT_ROOT_DOMAIN}`)\"\n            - \"traefik.http.routers.${PROJECT_NAME}-elasticsearch.tls=true\"\n        healthcheck:\n            test: \"curl --fail http://localhost:9200/_cat/health || exit 1\"\n            interval: 5s\n            timeout: 5s\n            retries: 5\n        profiles:\n            - default\n\n    kibana:\n        image: kibana:7.8.0\n        depends_on:\n            - elasticsearch\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-kibana.rule=Host(`kibana.${PROJECT_ROOT_DOMAIN}`)\"\n            - \"traefik.http.routers.${PROJECT_NAME}-kibana.tls=true\"\n        profiles:\n            - default\n```\n\nThen, you will be able to browse:\n\n* `https://kibana.<root_domain>`\n* `https://elasticsearch.<root_domain>`\n\nIn your application, you can use the following configuration:\n\n* scheme: `http`;\n* host: `elasticsearch`;\n* port: `9200`.\n\n</details>\n\n### How to use with Sylius\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nAdd the php extension `gd` to `infrastructure/docker/services/php/Dockerfile`\n\n```\nphp${PHP_VERSION}-gd \\\n```\n\nIf you want to create a new Sylius project, you need to enter a builder (`inv\nbuilder`) and run the following commands\n\n1. Remove the `application` folder:\n\n    ```bash\n    cd ..\n    rm -rf application/*\n    ```\n\n1. Create a new project:\n\n    ```bash\n    composer create-project sylius/sylius-standard application\n    ```\n\n1. Configure the `.env`\n\n    ```bash\n    sed -i 's#DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\\?serverVersion=12\\&charset=utf8#' application/.env\n    ```\n\n</details>\n\n### How to add RabbitMQ and its dashboard\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use RabbitMQ and its dashboard, you should add a new service:\n\n```Dockerfile\n# services/rabbitmq/Dockerfile\nFROM rabbitmq:3-management-alpine\n\nCOPY etc/. /etc/\n```\n\nAnd you can add specific RabbitMQ configuration in the `services/rabbitmq/etc/rabbitmq/rabbitmq.conf` file:\n```\n# services/rabbitmq/etc/rabbitmq/rabbitmq.conf\nvm_memory_high_watermark.absolute = 1GB\n```\n\nFinally, add the following content to the `docker-compose.yml` file:\n```yaml\nvolumes:\n    rabbitmq-data: {}\n\nservices:\n    rabbitmq:\n        build: services/rabbitmq\n        volumes:\n            - rabbitmq-data:/var/lib/rabbitmq\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-rabbitmq.rule=Host(`rabbitmq.${PROJECT_ROOT_DOMAIN}`)\"\n            - \"traefik.http.routers.${PROJECT_NAME}-rabbitmq.tls=true\"\n            - \"traefik.http.services.rabbitmq.loadbalancer.server.port=15672\"\n        healthcheck:\n            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\"\n            interval: 5s\n            timeout: 5s\n            retries: 5\n        profiles:\n            - default\n```\n\nIn order to publish and consume messages with PHP, you need to install the\n`php${PHP_VERSION}-amqp` in the `php` image.\n\nThen, you will be able to browse:\n\n* `https://rabbitmq.<root_domain>` (username: `guest`, password: `guest`)\n\nIn your application, you can use the following configuration:\n\n* host: `rabbitmq`;\n* username: `guest`;\n* password: `guest`;\n* port: `rabbitmq`.\n\nFor example in Symfony you can use: `MESSENGER_TRANSPORT_DSN=amqp://guest:guest@rabbitmq:5672/%2f/messages`.\n\n</details>\n\n### How to add Redis and its dashboard\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use Redis and its dashboard, you should add the following content to\nthe `docker-compose.yml` file:\n\n```yaml\nvolumes:\n    redis-data: {}\n    redis-insight-data: {}\n\nservices:\n    redis:\n        image: redis:5\n        healthcheck:\n            test: [\"CMD\", \"redis-cli\", \"ping\"]\n            interval: 5s\n            timeout: 5s\n            retries: 5\n        volumes:\n            - \"redis-data:/data\"\n        profiles:\n            - default\n\n    redis-insight:\n        image: redislabs/redisinsight\n        volumes:\n            - \"redis-insight-data:/db\"\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-redis.rule=Host(`redis.${PROJECT_ROOT_DOMAIN}`)\"\n            - \"traefik.http.routers.${PROJECT_NAME}-redis.tls=true\"\n        profiles:\n            - default\n\n```\n\nIn order to communicate with Redis, you need to install the\n`php${PHP_VERSION}-redis` in the `php` image.\n\nThen, you will be able to browse:\n\n* `https://redis.<root_domain>`\n\nIn your application, you can use the following configuration:\n\n* host: `redis`;\n* port: `6379`.\n\n</details>\n\n### How to add Mailpit\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use Mailpit and its dashboard, you should add the following content\nto the `docker-compose.yml` file:\n\n```yaml\nservices:\n    mail:\n        image: axllent/mailpit\n        environment:\n            - MP_SMTP_BIND_ADDR=0.0.0.0:25\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-mail.rule=Host(`mail.${PROJECT_ROOT_DOMAIN}`)\"\n            - \"traefik.http.routers.${PROJECT_NAME}-mail.tls=true\"\n            - \"traefik.http.services.mail.loadbalancer.server.port=8025\"\n        profiles:\n            - default\n```\n\nThen, you will be able to browse:\n\n* `https://mail.<root_domain>`\n\nIn your application, you can use the following configuration:\n\n* scheme: `smtp`;\n* host: `mail`;\n* port: `25`.\n\nFor example in Symfony you can use: `MAILER_DSN=smtp://mail:25`.\n\n</details>\n\n### How to add Mercure\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use Mercure, you should add the following content to the\n`docker-compose.yml` file:\n\n```yaml\nservices:\n    mercure:\n        image: dunglas/mercure\n        environment:\n            - \"MERCURE_PUBLISHER_JWT_KEY=password\"\n            - \"MERCURE_SUBSCRIBER_JWT_KEY=password\"\n            - \"ALLOW_ANONYMOUS=1\"\n            - \"CORS_ALLOWED_ORIGINS=*\"\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-mercure.rule=Host(`mercure.${PROJECT_ROOT_DOMAIN}`)\"\n            - \"traefik.http.routers.${PROJECT_NAME}-mercure.tls=true\"\n        profiles:\n            - default\n```\n\nIf you are using Symfony, you must put the following configuration in the `.env` file:\n\n```\nMERCURE_PUBLISH_URL=http://mercure/.well-known/mercure\nMERCURE_JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6W10sInB1Ymxpc2giOltdfX0.t9ZVMwTzmyjVs0u9s6MI7-oiXP-ywdihbAfPlghTBeQ\n```\n\n</details>\n\n### How to add redirection.io\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use redirection.io, you should add the following content to the\n`docker-compose.yml` file to run the agent:\n\n```yaml\nservices:\n    redirectionio-agent:\n        build: services/redirectionio-agent\n```\n\nAdd the following file `infrastructure/docker/services/redirectionio-agent/Dockerfile`:\n\n```Dockerfile\nFROM alpine:3.12 AS alpine\n\nWORKDIR /tmp\n\nRUN apk add --no-cache wget ca-certificates \\\n    && wget https://packages.redirection.io/dist/stable/2/any/redirectionio-agent-latest_any_amd64.tar.gz \\\n    && tar -xzvf redirectionio-agent-latest_any_amd64.tar.gz\n\nFROM scratch\n\n# Binary copied from tar\nCOPY --from=alpine /tmp/redirection-agent/redirectionio-agent /usr/local/bin/redirectionio-agent\n\n# Configuration, can be replaced by your own\nCOPY etc /etc\n\n# Root SSL Certificates, needed as we do HTTPS requests to our service\nCOPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCMD [\"/usr/local/bin/redirectionio-agent\"]\n```\n\nAdd `infrastructure/docker/services/redirectionio-agent/etc/redirectionio/agent.yml`:\n\n```yaml\ninstance_name: \"my-instance-dev\" ### You may want to change this\nlisten: 0.0.0.0:10301\n```\n\nThen you'll need `wget`. In\n`infrastructure/docker/services/php/Dockerfile`, in stage `frontend`:\n\n```Dockerfile\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        wget \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n```\n\nYou can group this command with another one.\n\nThen, **after** installing nginx, you need to install the module:\n\n```Dockerfile\nRUN wget -q -O - https://packages.redirection.io/gpg.key | gpg --dearmor > /usr/share/keyrings/redirection.io.gpg \\\n    && 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 \\\n    && apt-get update \\\n    && apt-get install libnginx-mod-redirectionio \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n```\n\nFinally, you need to edit\n`infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` to add the\nfollowing configuration in the `server` block:\n\n```\nredirectionio_pass redirectionio-agent:10301;\nredirectionio_project_key \"AAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBB\";\n```\n\n**Don't forget to change the project key**.\n\n</details>\n\n### How to add Blackfire.io\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to use Blackfire.io, you should add the following content to the\n`docker-compose.yml` file to run the agent:\n\n```yaml\nservices:\n    blackfire:\n        image: blackfire/blackfire\n        environment:\n            BLACKFIRE_SERVER_ID: FIXME\n            BLACKFIRE_SERVER_TOKEN: FIXME\n            BLACKFIRE_CLIENT_ID: FIXME\n            BLACKFIRE_CLIENT_TOKEN: FIXME\n        profiles:\n            - default\n\n```\n\nThen you'll need `wget`. In\n`infrastructure/docker/services/php/Dockerfile`, in stage `base`:\n\n```Dockerfile\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        wget \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n```\n\nYou can group this command with another one.\n\nThen, **after** installing PHP, you need to install the probe:\n\n```Dockerfile\nRUN wget -q -O - https://packages.blackfire.io/gpg.key | gpg --dearmor > /usr/share/keyrings/blackfire.io.gpg \\\n    && 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' \\\n    && apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        blackfire-php \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \\\n    && sed -i 's#blackfire.agent_socket.*#blackfire.agent_socket=tcp://blackfire:8707#' /etc/php/${PHP_VERSION}/mods-available/blackfire.ini\n```\n\nIf you want to profile HTTP calls, you need to enable the probe with PHP-FPM.\nSo in `infrastructure/docker/services/php/Dockerfile`:\n\n```Dockerfile\nRUN phpenmod blackfire\n```\n\nHere also, You can group this command with another one.\n\n</details>\n\n### How to add support for crons?\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to set up crontab, you should add a new container:\n\n```Dockerfile\n# services/php/Dockerfile\n\nFROM php-base AS cron\n\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        cron \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n\nCOPY --link cron/crontab /etc/cron.d/crontab\nCOPY --link cron/entrypoint.sh /entrypoint.sh\nENTRYPOINT [\"/entrypoint.sh\"]\n\nCMD [\"cron\", \"-f\"]\n```\n\nAnd you can add all your crons in the `services/php/crontab` file:\n```crontab\n* * * * * php -r 'echo time().PHP_EOL;' > /tmp/cron-stdout 2>&1\n```\n\nAnd you can add the following content to the `services/php/entrypoint.sh` file:\n```bash\n#!/bin/bash\nset -e\n\ngroupadd -g $USER_ID app\nuseradd -M -u $USER_ID -g $USER_ID -s /bin/bash app\n\ncrontab -u app /etc/cron.d/crontab\n\n# Wrapper for logs\nFIFO=/tmp/cron-stdout\nrm -f $FIFO\nmkfifo $FIFO\nchmod 0666 $FIFO\nwhile true; do\n  cat /tmp/cron-stdout\ndone &\n\nexec \"$@\"\n```\n\nFinally, add the following content to the `docker-compose.yml` file:\n```yaml\nservices:\n    cron:\n        build:\n            context: services/php\n            target: cron\n            cache_from:\n                - \"type=registry,ref=${REGISTRY:-}/cron:cache\"\n        # depends_on:\n        #     postgres:\n        #         condition: service_healthy\n        env_file: .env\n        environment:\n            USER_ID: ${USER_ID}\n        volumes:\n            - \"../..:/var/www:cached\"\n            - \"../../.home:/home/app:cached\"\n        profiles:\n            - default\n```\n\n</details>\n\n### How to run workers?\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to set up workers, you should define their services in the `docker-compose.worker.yml` file:\n\n```yaml\nservices:\n    worker_my_worker:\n        <<: *worker_base\n        command: /var/www/application/my-worker\n\n    worker_date:\n        <<: *worker_base\n        command: watch -n 1 date\n```\n\n</details>\n\n### How to use PHP FPM status page?\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIf you want to use the [PHP FPM status\npage](https://www.php.net/manual/en/fpm.status.php) you need to remove a\nconfiguration block in the\n`infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` file:\n\n```diff\n-        # Remove this block if you want to access to PHP FPM monitoring\n-        # dashboarsh (on URL: /php-fpm-status). WARNING: on production, you must\n-        # secure this page (by user IP address, with a password, for example)\n-        location ~ ^/php-fpm-status$ {\n-            deny all;\n-        }\n-\n```\n\nAnd if your application uses the front controller pattern, and you want to see\nthe real request URI, you also need to uncomment the following configuration\nblock:\n\n```diff\n-            # # Uncomment if you want to use /php-fpm-status endpoint **with**\n-            # # real request URI. It may have some side effects, that's why it's\n-            # # commented by default\n-            # fastcgi_param SCRIPT_NAME $request_uri;\n+            # Uncomment if you want to use /php-fpm-status endpoint **with**\n+            # real request URI. It may have some side effects, that's why it's\n+            # commented by default\n+            fastcgi_param SCRIPT_NAME $request_uri;\n```\n\n</details>\n\n### How to pg_activity for monitoring PostgreSQL\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nIn order to install pg_activity, you should add the following content to the\n`infrastructure/docker/services/postgres/Dockerfile` file:\n\n```Dockerfile\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        pg-activity \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n```\n\nThen, you can add the following content to the `castor.php` file:\n\n```php\n#[AsTask(description: 'Monitor PostgreSQL', namespace: 'app:db')]\nfunction pg_activity(): void\n{\n    docker_compose('exec postgres pg_activity -U app');\n}\n```\n\nFinally you can use the following command:\n\n```\ncastor app:db:pg-activity\n```\n\n</details>\n\n### Docker For Windows support\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nThis 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:\n\n- You will be prompted to run the env vars manually if you use PowerShell.\n</details>\n\n### How to access a container via hostname from another container\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nLet's say you have a container (`frontend`) that responds to many hostnames:\n`app.test`, `api.app.test`, `admin.app.test`. And you have another container\n(`builder`) that needs to call the `frontend` with a specific hostname - or with\nHTTPS. This is usually the case when you have a functional test suite.\n\nTo enable this feature, you need to add `extra_hosts` to the `builder` container\nlike so:\n\n```yaml\nservices:\n    builder:\n        # [...]\n        extra_hosts:\n            - \"app.test:host-gateway\"\n            - \"api.app.test:host-gateway\"\n            - \"admin.app.test:host-gateway\"\n```\n\n</details>\n\n### How to connect networks of two projects\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nLet's say you have two projects `foo` and `bar`. You want to run both projects a\nthe same time. And containers from `foo` project should be able to dialog with\n`bar` project via public network (host network).\n\nIn the `foo` project, you'll need to declare the `bar_default` network in\n`docker-compose.yml`:\n\n```yaml\nnetworks:\n    bar_default:\n        external: true\n```\n\nThen, attach it to the the `foo` router:\n\n```yaml\nservices:\n    router:\n        networks:\n            - default\n            - bar_default\n```\n\nFinally, you must remove the constraints on the router so it'll be able to\ndiscover containers from another docker compose project:\n\n```diff\n--- a/infrastructure/docker/services/router/traefik/traefik.yaml\n+++ b/infrastructure/docker/services/router/traefik/traefik.yaml\n providers:\n   docker:\n     exposedByDefault: false\n-    constraints: \"Label(`project-name`,`{{ PROJECT_NAME }}`)\"\n   file:\n```\n\nFinally, you must :\n\n1. build the project `foo`\n1. build the project `bar`\n1.  Create the network `bar_default` (first time only)\n    ```\n    docker network create bar_default\n    ```\n1. start the project `foo`\n1. start the project `bar`\n\n</details>\n\n### How to use FrankenPHP\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nMigrating to FrankenPHP involves a lot of changes. You can take inspiration from\nthe following [repostory](https://github.com/lyrixx/async-messenger-mercure)\nand specifically [this commit](https://github.com/lyrixx/async-messenger-mercure/commit/9ac8776253f3950a6c57d457b3742923f9e096a7).\n\n</details>\n\n### How to use a docker registry to cache images layer\n\n<details>\n\n<summary>Read the cookbook</summary>\n\nYou can use a docker registry to cache images layer, it can be useful to speed\nup the build process during the CI and local development.\n\nFirst you need a docker registry, in following examples we will use the GitHub\nregistry (ghcr.io).\n\nThen add the registry to the context variable of the `castor.php` file:\n\n```php\nfunction create_default_variables(): Context\n{\n    return [\n        // [...]\n        'registry' => 'ghcr.io/your-organization/your-project',\n    ];\n}\n```\n\nOnce you have the registry, you can push the images to the registry:\n\n```bash\ncastor docker:push\n```\n\n> Pushing image cache from a dev environment to a registry is not recommended,\n> as cache may be sensitive to the environment and may not be compatible with\n> other environments. It happens, for example, when you add some build args\n> depending on your environment. It is recommended to push the cache from the CI\n> environment.\n\nThis command will generate a bake file with the images to push from the\n`cache_from` directive of the `docker-compose.yml` file. If you want to add more\nimages to push, you can add the `cache_from` directive to them.\n\n```yaml\nservices:\n    my-service:\n        build:\n            cache_from:\n                - \"type=registry,ref=${REGISTRY:-}/my-service:cache\"\n```\n\n#### How to use cached images in a GitHub action\n\n##### Pushing images to the registry from a GitHub action\n\n1. Ensure that the github token have the `write:packages` scope:\n\n```yaml\npermissions:\n    contents: read\n    packages: write\n```\n\n2. Install Docker buildx in the github action:\n\n```yaml\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v2\n```\n\n3. Login to the registry:\n\n```yaml\n    - name: Log in to registry\n      shell: bash\n      run: echo \"${{ secrets.GITHUB_TOKEN }}\" | docker login ghcr.io -u $ --password-stdin\n```\n\n##### Using the cached images in GitHub action\n\nBy default images are private in the GitHub registry, you will need to login to\nthe registry to pull the images:\n\n```yaml\n    - name: Log in to registry\n      shell: bash\n      run: echo \"${{ secrets.GITHUB_TOKEN }}\" | docker login ghcr.io -u $ --password-stdin\n```\n\n</details>\n\n## Credits\n\nDocker-starter logo was created by [Caneco](https://twitter.com/caneco).\n\n<br><br>\n<div align=\"center\">\n<a href=\"https://jolicode.com/\"><img src=\"https://jolicode.com/media/original/oss/footer-github.png?v3\" alt=\"JoliCode is sponsoring this project\"></a>\n</div>\n"
  },
  {
    "path": "application/public/index.php",
    "content": "<?php\n\necho 'Hello world from PHP ', \\PHP_MAJOR_VERSION, '.', \\PHP_MINOR_VERSION, '.', \\PHP_RELEASE_VERSION, \" inside Docker! \\n\";\n\necho 'Environment: ', $_SERVER['APP_ENV'] ?? 'not set', \"\\n\";\n"
  },
  {
    "path": "castor.php",
    "content": "<?php\n\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\guard_min_version;\nuse function Castor\\import;\nuse function Castor\\io;\nuse function Castor\\notify;\nuse function Castor\\variable;\nuse function docker\\about;\nuse function docker\\build;\nuse function docker\\docker_compose_run;\nuse function docker\\up;\n\n// use function docker\\workers_start;\n// use function docker\\workers_stop;\n\nguard_min_version('0.26.0');\n\nimport(__DIR__ . '/.castor');\n\n/**\n * @return array{project_name: string, root_domain: string, extra_domains: string[], php_version: string}\n */\nfunction create_default_variables(): array\n{\n    $projectName = 'app';\n    $tld = 'test';\n\n    return [\n        'project_name' => $projectName,\n        'root_domain' => \"{$projectName}.{$tld}\",\n        'extra_domains' => [\n            \"www.{$projectName}.{$tld}\",\n        ],\n        // In order to test docker stater, we need a way to pass different values.\n        // You should remove the `$_SERVER` and hardcode your configuration.\n        'php_version' => $_SERVER['DS_PHP_VERSION'] ?? '8.5',\n        'registry' => $_SERVER['DS_REGISTRY'] ?? null,\n    ];\n}\n\n#[AsTask(description: 'Builds and starts the infrastructure, then install the application (composer, yarn, ...)')]\nfunction start(): void\n{\n    io()->title('Starting the stack');\n\n    // workers_stop();\n    build();\n    install();\n    up(profiles: ['default']); // We can't start worker now, they are not installed\n    migrate();\n    // workers_start();\n\n    notify('The stack is now up and running.');\n    io()->success('The stack is now up and running.');\n\n    about();\n}\n\n#[AsTask(description: 'Installs the application (composer, yarn, ...)', namespace: 'app', aliases: ['install'])]\nfunction install(): void\n{\n    io()->title('Installing the application');\n\n    $basePath = sprintf('%s/application', variable('root_dir'));\n\n    if (is_file(\"{$basePath}/composer.json\")) {\n        io()->section('Installing PHP dependencies');\n        docker_compose_run('composer install -n --prefer-dist --optimize-autoloader');\n    }\n    if (is_file(\"{$basePath}/yarn.lock\")) {\n        io()->section('Installing Node.js dependencies');\n        docker_compose_run('yarn install --frozen-lockfile');\n    } elseif (is_file(\"{$basePath}/package.json\")) {\n        io()->section('Installing Node.js dependencies');\n\n        if (is_file(\"{$basePath}/package-lock.json\")) {\n            docker_compose_run('npm ci');\n        } else {\n            docker_compose_run('npm install');\n        }\n    }\n    if (is_file(\"{$basePath}/importmap.php\")) {\n        io()->section('Installing importmap');\n        docker_compose_run('bin/console importmap:install');\n    }\n\n    qa\\install();\n}\n\n#[AsTask(description: 'Update dependencies')]\nfunction update(bool $withTools = false): void\n{\n    io()->title('Updating dependencies...');\n\n    // docker_compose_run('composer update -o');\n\n    if ($withTools) {\n        qa\\update();\n    }\n}\n\n#[AsTask(description: 'Clears the application cache', namespace: 'app', aliases: ['cache-clear'])]\nfunction cache_clear(bool $warm = true): void\n{\n    // io()->title('Clearing the application cache');\n\n    // docker_compose_run('rm -rf var/cache/');\n\n    // if ($warm) {\n    //     cache_warmup();\n    // }\n}\n\n#[AsTask(description: 'Warms the application cache', namespace: 'app', aliases: ['cache-warmup'])]\nfunction cache_warmup(): void\n{\n    // io()->title('Warming the application cache');\n\n    // docker_compose_run('bin/console cache:warmup', c: context()->withAllowFailure());\n}\n\n#[AsTask(description: 'Migrates database schema', namespace: 'app:db', aliases: ['migrate'])]\nfunction migrate(): void\n{\n    // io()->title('Migrating the database schema');\n\n    // docker_compose_run('bin/console doctrine:database:create --if-not-exists');\n    // docker_compose_run('bin/console doctrine:migration:migrate -n --allow-no-migration --all-or-nothing');\n}\n\n#[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['fixtures'])]\nfunction fixtures(): void\n{\n    // io()->title('Loads fixtures');\n\n    // docker_compose_run('bin/console doctrine:fixture:load -n');\n}\n"
  },
  {
    "path": "infrastructure/docker/docker-compose.dev.yml",
    "content": "services:\n    router:\n        build: services/router\n        volumes:\n            - \"/var/run/docker.sock:/var/run/docker.sock\"\n            - \"./services/router/certs:/etc/ssl/certs\"\n        ports:\n            - \"80:80\"\n            - \"443:443\"\n            - \"8080:8080\"\n        networks:\n            - default\n        profiles:\n            - default\n"
  },
  {
    "path": "infrastructure/docker/docker-compose.yml",
    "content": "# Templates to factorize the service definitions\nx-templates:\n    worker_base: &worker_base\n        build:\n            context: services/php\n            target: worker\n        user: \"${USER_ID}:${USER_ID}\"\n        environment:\n            - APP_ENV\n        depends_on:\n            postgres:\n                condition: service_healthy\n        volumes:\n            - \"../..:/var/www:cached\"\n        profiles:\n            - worker\n\nvolumes:\n    postgres-data: {}\n    # # Needed if $XDG_ env vars have been overridden\n    # builder-yarn-data: {}\n\nservices:\n    postgres:\n        image: postgres:16\n        environment:\n            - POSTGRES_USER=app\n            - POSTGRES_PASSWORD=app\n        volumes:\n            - postgres-data:/var/lib/postgresql/data\n        healthcheck:\n            test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n            interval: 5s\n            timeout: 5s\n            retries: 5\n        profiles:\n            - default\n\n    frontend:\n        build:\n            context: services/php\n            target: frontend\n            cache_from:\n              - \"type=registry,ref=${REGISTRY:-}/frontend:cache\"\n        user: \"${USER_ID}:${USER_ID}\"\n        environment:\n            - APP_ENV\n        volumes:\n            - \"../..:/var/www:cached\"\n            - \"../../.home:/home/app:cached\"\n        depends_on:\n            postgres:\n                condition: service_healthy\n        profiles:\n            - default\n        labels:\n            - \"traefik.enable=true\"\n            - \"project-name=${PROJECT_NAME}\"\n            - \"traefik.http.routers.${PROJECT_NAME}-frontend.rule=Host(${PROJECT_DOMAINS})\"\n            - \"traefik.http.routers.${PROJECT_NAME}-frontend.tls=true\"\n            - \"traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.rule=Host(${PROJECT_DOMAINS})\"\n            # Comment the next line to be able to access frontend via HTTP instead of HTTPS\n            - \"traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file\"\n\n    # worker_messenger:\n    #     <<: *worker_base\n    #     command: php -d memory_limit=1G /var/www/application/bin/console messenger:consume async --memory-limit=128M\n\n    builder:\n        build:\n            context: services/php\n            target: builder\n            cache_from:\n                - \"type=registry,ref=${REGISTRY:-}/builder:cache\"\n        init: true\n        user: \"${USER_ID}:${USER_ID}\"\n        environment:\n            - APP_ENV\n            # The following list contains the common environment variables exposed by CI platforms\n            - GITHUB_ACTIONS\n            - CI # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari\n            - CONTINUOUS_INTEGRATION # Travis CI, Cirrus CI\n            - BUILD_NUMBER # Jenkins, TeamCity\n            - RUN_ID # TaskCluster, dsari\n        volumes:\n            - \"../..:/var/www:cached\"\n            - \"../../.home:/home/app:cached\"\n            # Needed when $XDG_ env vars have overridden, to persist the yarn\n            # cache between builder and watcher, adapt according to the location\n            # of $XDG_DATA_HOME\n            # - \"builder-yarn-data:/data/yarn\"\n        depends_on:\n            - postgres\n        profiles:\n            - builder\n"
  },
  {
    "path": "infrastructure/docker/services/php/Dockerfile",
    "content": "# hadolint global ignore=DL3008\n\nFROM debian:12.8-slim AS php-base\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        curl \\\n        ca-certificates \\\n        gnupg \\\n    && curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb \\\n    && dpkg -i /tmp/debsuryorg-archive-keyring.deb \\\n    && 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 \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        bash-completion \\\n        procps \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n\nARG PHP_VERSION\n\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        \"php${PHP_VERSION}-apcu\" \\\n        \"php${PHP_VERSION}-bcmath\" \\\n        \"php${PHP_VERSION}-cli\" \\\n        \"php${PHP_VERSION}-common\" \\\n        \"php${PHP_VERSION}-curl\" \\\n        \"php${PHP_VERSION}-iconv\" \\\n        \"php${PHP_VERSION}-intl\" \\\n        \"php${PHP_VERSION}-mbstring\" \\\n        \"php${PHP_VERSION}-pgsql\" \\\n        \"php${PHP_VERSION}-uuid\" \\\n        \"php${PHP_VERSION}-xml\" \\\n        \"php${PHP_VERSION}-zip\" \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n\n# Configuration\nCOPY base/php-configuration /etc/php/${PHP_VERSION}\n\nENV PHP_VERSION=${PHP_VERSION}\nENV HOME=/home/app\nENV COMPOSER_MEMORY_LIMIT=-1\n\nWORKDIR /var/www\n\nFROM php-base AS frontend\n\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        nginx \\\n        \"php${PHP_VERSION}-fpm\" \\\n        runit \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \\\n    && rm -r \"/etc/php/${PHP_VERSION}/fpm/pool.d/\"\n\nRUN useradd -s /bin/false nginx\n\nCOPY frontend/php-configuration /etc/php/${PHP_VERSION}\nCOPY frontend/etc/nginx/. /etc/nginx/\nRUN rm -rf /etc/service/\nCOPY frontend/etc/service/. /etc/service/\nRUN chmod 777 /etc/service/*/supervise/\n\nRUN phpenmod app-default \\\n    && phpenmod app-fpm\n\nEXPOSE 80\n\nCMD [\"runsvdir\", \"-P\", \"/etc/service\"]\n\nFROM php-base AS worker\n\nFROM php-base AS builder\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nARG NODEJS_VERSION=24.x\nRUN curl -s https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \\\n    && 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\n\n# Default toys\nENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        \"git\" \\\n        \"make\" \\\n        \"nodejs\" \\\n        \"php${PHP_VERSION}-dev\" \\\n        \"sudo\" \\\n        \"unzip\" \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \\\n    && corepack enable \\\n    && yarn set version stable\n\n# Install a fake sudo command\n# 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\n# Use it at your own risk\n# COPY base/sudo.sh /usr/local/bin/sudo\n# RUN curl -L https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64 -o /usr/local/bin/gosu && \\\n#    chmod u+s /usr/local/bin/gosu && \\\n#    chmod +x /usr/local/bin/gosu && \\\n#    chmod +x /usr/local/bin/sudo\n\n# Config\nCOPY builder/php-configuration /etc/php/${PHP_VERSION}\nRUN phpenmod app-default \\\n    && phpenmod app-builder\n\n# Composer\nCOPY --from=composer/composer:2.9.5 /usr/bin/composer /usr/bin/composer\n\n# Pie\nRUN curl -L --output /usr/local/bin/pie https://github.com/php/pie/releases/download/1.3.9/pie.phar \\\n    && chmod +x /usr/local/bin/pie\n\n# Autocompletion\nADD https://raw.githubusercontent.com/symfony/symfony/refs/heads/7.3/src/Symfony/Component/Console/Resources/completion.bash /tmp/completion.bash\n\n# Composer symfony/console version is too old, and doest not support \"API version feature\", so we remove it\n# Hey, while we are at it, let's add some more completion\nRUN sed /tmp/completion.bash \\\n        -e \"s/{{ COMMAND_NAME }}/composer/g\" \\\n        -e 's/\"-a{{ VERSION }}\"//g' \\\n        -e \"s/{{ VERSION }}/1/g\"  \\\n        > /etc/bash_completion.d/composer \\\n    && sed /tmp/completion.bash \\\n        -e \"s/{{ COMMAND_NAME }}/console/g\" \\\n        -e \"s/{{ VERSION }}/1/g\"  \\\n        > /etc/bash_completion.d/console\n\n# Third party tools\nENV PATH=\"$PATH:/var/www/tools/bin\"\n\n# Good default customization\nRUN cat >> /etc/bash.bashrc <<EOF\n. /etc/bash_completion\n\nPS1='\\[\\e[01;33m\\]\\u \\[\\e[00;32m\\]\\w\\[\\e[0m\\] '\nEOF\n\nWORKDIR /var/www/application\n"
  },
  {
    "path": "infrastructure/docker/services/php/base/php-configuration/mods-available/app-default.ini",
    "content": "; priority=30\n[PHP]\nshort_open_tag = Off\nmemory_limit = 512M\nerror_reporting = E_ALL\ndisplay_errors = On\ndisplay_startup_errors = On\nerror_log = /proc/self/fd/2\nlog_errors = On\nlog_errors_max_len = 0\nmax_execution_time = 0\nalways_populate_raw_post_data = -1\nupload_max_filesize = 20M\npost_max_size = 20M\n[Date]\ndate.timezone = UTC\n[Phar]\nphar.readonly = Off\n[opcache]\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nrealpath_cache_size=4096K\nrealpath_cache_ttl=600\nopcache.error_log = /proc/self/fd/2\n[apc]\napc.enabled=1\napc.enable_cli=1\n"
  },
  {
    "path": "infrastructure/docker/services/php/base/sudo.sh",
    "content": "#!/usr/bin/env bash\n\nexport GOSU_PLEASE_LET_ME_BE_COMPLETELY_INSECURE_I_GET_TO_KEEP_ALL_THE_PIECES=\"I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die.\"\nexec gosu 0:0 $@\n"
  },
  {
    "path": "infrastructure/docker/services/php/builder/php-configuration/mods-available/app-builder.ini",
    "content": "; priority=40\n[PHP]\nerror_log = /var/log/php/error.log\n[opcache]\nopcache.error_log = /var/log/php/opcache.log\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/nginx/environments",
    "content": ""
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf",
    "content": "user      nginx;\npid       /tmp/nginx.pid;\ndaemon    off;\nerror_log /proc/self/fd/2;\ninclude /etc/nginx/modules-enabled/*.conf;\n\nhttp {\n    access_log /proc/self/fd/1;\n    sendfile on;\n    tcp_nopush on;\n    tcp_nodelay on;\n    keepalive_timeout 65;\n    types_hash_max_size 2048;\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n    client_max_body_size 20m;\n    server_tokens off;\n\n    gzip on;\n    gzip_disable \"msie6\";\n    gzip_vary on;\n    gzip_proxied any;\n    gzip_comp_level 6;\n    gzip_buffers 16 8k;\n    gzip_http_version 1.1;\n    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;\n\n    client_body_temp_path /tmp/nginx-client_body_temp_path;\n    fastcgi_temp_path /tmp/nginx-fastcgi_temp_path;\n    proxy_temp_path /tmp/nginx-proxy_temp_path;\n    scgi_temp_path /tmp/nginx-scgi_temp_path;\n    uwsgi_temp_path /tmp/nginx-uwsgi_temp_path;\n\n    server {\n        listen 0.0.0.0:80;\n        root /var/www/application/public;\n\n        location ~* \\.(jpg|jpeg|png|gif|ico|css|js|svg)$ {\n            access_log off;\n            add_header Cache-Control \"no-cache\";\n        }\n\n        # Remove this block if you want to access to PHP FPM monitoring\n        # dashboard (on URL: /php-fpm-status). WARNING: on production, you must\n        # secure this page (by user IP address, with a password, for example)\n        location ~ ^/php-fpm-status$ {\n            deny all;\n            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n            fastcgi_index index.php;\n            include fastcgi_params;\n            fastcgi_pass 127.0.0.1:9000;\n        }\n\n        location / {\n            # try to serve file directly, fallback to index.php\n            try_files $uri /index.php$is_args$args;\n        }\n\n        location ~ ^/index\\.php(/|$) {\n            fastcgi_pass 127.0.0.1:9000;\n            fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n\n            include fastcgi_params;\n            include environments;\n\n            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n            fastcgi_param HTTPS on;\n            fastcgi_param SERVER_NAME $http_host;\n            # # Uncomment if you want to use /php-fpm-status endpoint **with**\n            # # real request URI. It may have some side effects, that's why it's\n            # # commented by default\n            # fastcgi_param SCRIPT_NAME $request_uri;\n        }\n\n        error_log  /proc/self/fd/2;\n        access_log /proc/self/fd/1;\n    }\n}\n\nevents {}\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/nginx/run",
    "content": "#!/bin/sh\n\nexec /usr/sbin/nginx\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore",
    "content": "/*\n!.gitignore\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/php-fpm/run",
    "content": "#!/bin/sh\n\nexec /usr/sbin/php-fpm${PHP_VERSION} -y /etc/php/${PHP_VERSION}/fpm/php-fpm.conf -O\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/php-fpm/supervise/.gitignore",
    "content": "/*\n!.gitignore\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf",
    "content": "[global]\nerror_log  = /proc/self/fd/2\ndaemonize  = no\n\n[www]\nlisten = 127.0.0.1:9000\npm = dynamic\npm.max_children = 25\npm.start_servers = 2\npm.min_spare_servers = 2\npm.max_spare_servers = 3\npm.max_requests = 500\npm.status_path = /php-fpm-status\nclear_env  = no\nrequest_terminate_timeout = 120s\ncatch_workers_output = yes\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/php-configuration/mods-available/app-fpm.ini",
    "content": "; priority=40\n[PHP]\nexpose_php = off\nmemory_limit = 128M\nmax_execution_time = 30\n"
  },
  {
    "path": "infrastructure/docker/services/router/Dockerfile",
    "content": "FROM traefik:v3.6.1\n\nCOPY traefik /etc/traefik\n\nARG PROJECT_NAME\nRUN sed -i \"s/{{ PROJECT_NAME }}/${PROJECT_NAME}/g\" /etc/traefik/traefik.yaml\n\nVOLUME [ \"/etc/ssl/certs\" ]\n"
  },
  {
    "path": "infrastructure/docker/services/router/certs/.gitkeep",
    "content": ""
  },
  {
    "path": "infrastructure/docker/services/router/generate-ssl.sh",
    "content": "#!/usr/bin/env bash\n\n# Script used in dev to generate a basic SSL cert\n\nBASE=$(dirname $0)\n\nCERTS_DIR=$BASE/certs\n\nrm -rf $CERTS_DIR\nmkdir -p $CERTS_DIR\ntouch $CERTS_DIR/.gitkeep\n\nopenssl req -x509 -sha256 -newkey rsa:4096 \\\n    -keyout $CERTS_DIR/key.pem \\\n    -out $CERTS_DIR/cert.pem \\\n    -days 3650 -nodes -config \\\n    $BASE/openssl.cnf\n"
  },
  {
    "path": "infrastructure/docker/services/router/openssl.cnf",
    "content": "# Configuration used in dev to generate a basic SSL cert\n[req]\ndefault_bits = 2048\nprompt = no\ndefault_md = sha256\ndistinguished_name = dn\nx509_extensions\t= v3_req\n\n[v3_req]\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always,issuer:always\nbasicConstraints=CA:true\nsubjectAltName = @alt_names\n\n[dn]\nCN=app.test\n\n[alt_names]\nDNS.1 = app.test\n"
  },
  {
    "path": "infrastructure/docker/services/router/traefik/dynamic_conf.yaml",
    "content": "tls:\n  stores:\n    default:\n      defaultCertificate:\n        certFile: /etc/ssl/certs/cert.pem\n        keyFile: /etc/ssl/certs/key.pem\n\nhttp:\n  middlewares:\n    redirect-to-https:\n      redirectScheme:\n        scheme: https\n"
  },
  {
    "path": "infrastructure/docker/services/router/traefik/traefik.yaml",
    "content": "global:\n  checkNewVersion: false\n  sendAnonymousUsage: false\n\nproviders:\n  docker:\n    exposedByDefault: false\n    constraints: \"Label(`project-name`,`{{ PROJECT_NAME }}`)\"\n  file:\n    filename: /etc/traefik/dynamic_conf.yaml\n\n# # Uncomment get all DEBUG logs\n#log:\n#    level: \"DEBUG\"\n\n# # Uncomment to view all access logs\n#accessLog: {}\n\napi:\n  dashboard: true\n  insecure: true # No authentication are required\n\nentryPoints:\n  http:\n    address: \":80\"\n  https:\n    address: \":443\"\n  traefik: # this one exists by default\n    address: \":8080\"\n"
  },
  {
    "path": "phpstan.neon",
    "content": "parameters:\n    level: 8\n    paths:\n        # - application/src\n        - application/public\n        - castor.php\n        - .castor/\n    scanFiles:\n        - .castor.stub.php\n    # scanDirectories:\n    #     - application/vendor\n    tmpDir: tools/phpstan/var\n    inferPrivatePropertyTypeFromConstructor: true\n\n    # symfony:\n    #     containerXmlPath: 'application/var/cache/dev/App_KernelDevDebugContainer.xml'\n\n    typeAliases:\n        ContextData: '''\n            array{\n                project_name: string,\n                root_domain: string,\n                extra_domains: string[],\n                php_version: string,\n                docker_compose_files: list<string>,\n                docker_compose_run_environment: list<string>,\n                macos: bool,\n                power_shell: bool,\n                user_id: int,\n                root_dir: string,\n                registry?: ?string,\n            }\n        '''\n"
  },
  {
    "path": "tools/php-cs-fixer/.gitignore",
    "content": "/vendor/\n"
  },
  {
    "path": "tools/php-cs-fixer/composer.json",
    "content": "{\n    \"type\": \"project\",\n    \"require\": {\n        \"friendsofphp/php-cs-fixer\": \"^3.76.0\"\n    },\n    \"config\": {\n        \"platform\": {\n            \"php\": \"8.3\"\n        },\n        \"bump-after-update\": true,\n        \"sort-packages\": true\n    }\n}\n"
  },
  {
    "path": "tools/phpstan/.gitignore",
    "content": "/var/\n/vendor/\n"
  },
  {
    "path": "tools/phpstan/composer.json",
    "content": "{\n    \"type\": \"project\",\n    \"require\": {\n        \"phpstan/extension-installer\": \"^1.4.3\",\n        \"phpstan/phpstan\": \"^2.1.17\",\n        \"phpstan/phpstan-deprecation-rules\": \"^2.0.3\",\n        \"phpstan/phpstan-symfony\": \"^2.0.6\"\n    },\n    \"config\": {\n        \"allow-plugins\": {\n            \"phpstan/extension-installer\": true\n        },\n        \"bump-after-update\": true,\n        \"platform\": {\n            \"php\": \"8.3\"\n        },\n        \"sort-packages\": true\n    }\n}\n"
  },
  {
    "path": "tools/twig-cs-fixer/.gitignore",
    "content": "/vendor/\n"
  },
  {
    "path": "tools/twig-cs-fixer/composer.json",
    "content": "{\n    \"type\": \"project\",\n    \"require\": {\n        \"vincentlanglet/twig-cs-fixer\": \"^3.8.1\"\n    },\n    \"config\": {\n        \"platform\": {\n            \"php\": \"8.3\"\n        },\n        \"bump-after-update\": true,\n        \"sort-packages\": true\n    }\n}\n"
  }
]