Full Code of jolicode/docker-starter for AI

main cb03fe0c3427 cached
45 files
94.2 KB
26.1k tokens
39 symbols
1 requests
Download .txt
Repository: jolicode/docker-starter
Branch: main
Commit: cb03fe0c3427
Files: 45
Total size: 94.2 KB

Directory structure:
gitextract_is9p0cpd/

├── .castor/
│   ├── context.php
│   ├── database.php
│   ├── docker.php
│   ├── init.php
│   └── qa.php
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── cache.yml
│       └── ci.yml
├── .gitignore
├── .home/
│   └── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.dist.md
├── README.md
├── application/
│   └── public/
│       └── index.php
├── castor.php
├── infrastructure/
│   └── docker/
│       ├── docker-compose.dev.yml
│       ├── docker-compose.yml
│       └── services/
│           ├── php/
│           │   ├── Dockerfile
│           │   ├── base/
│           │   │   ├── php-configuration/
│           │   │   │   └── mods-available/
│           │   │   │       └── app-default.ini
│           │   │   └── sudo.sh
│           │   ├── builder/
│           │   │   └── php-configuration/
│           │   │       └── mods-available/
│           │   │           └── app-builder.ini
│           │   └── frontend/
│           │       ├── etc/
│           │       │   ├── nginx/
│           │       │   │   ├── environments
│           │       │   │   └── nginx.conf
│           │       │   └── service/
│           │       │       ├── nginx/
│           │       │       │   ├── run
│           │       │       │   └── supervise/
│           │       │       │       └── .gitignore
│           │       │       └── php-fpm/
│           │       │           ├── run
│           │       │           └── supervise/
│           │       │               └── .gitignore
│           │       └── php-configuration/
│           │           ├── fpm/
│           │           │   └── php-fpm.conf
│           │           └── mods-available/
│           │               └── app-fpm.ini
│           └── router/
│               ├── Dockerfile
│               ├── certs/
│               │   └── .gitkeep
│               ├── generate-ssl.sh
│               ├── openssl.cnf
│               └── traefik/
│                   ├── dynamic_conf.yaml
│                   └── traefik.yaml
├── phpstan.neon
└── tools/
    ├── php-cs-fixer/
    │   ├── .gitignore
    │   └── composer.json
    ├── phpstan/
    │   ├── .gitignore
    │   └── composer.json
    └── twig-cs-fixer/
        ├── .gitignore
        └── composer.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .castor/context.php
================================================
<?php

namespace docker;

use Castor\Attribute\AsContext;
use Castor\Context;
use Symfony\Component\Process\Process;

use function Castor\log;

#[AsContext(default: true)]
function create_default_context(): Context
{
    $data = create_default_variables() + [
        'project_name' => 'app',
        'root_domain' => 'app.test',
        'extra_domains' => [],
        'php_version' => '8.5',
        'docker_compose_files' => [
            'docker-compose.yml',
            'docker-compose.dev.yml',
        ],
        'docker_compose_run_environment' => [],
        'macos' => false,
        'power_shell' => false,
        // check if posix_geteuid is available, if not, use getmyuid (windows)
        'user_id' => \function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(),
        'root_dir' => \dirname(__DIR__),
    ];

    if (file_exists($data['root_dir'] . '/infrastructure/docker/docker-compose.override.yml')) {
        $data['docker_compose_files'][] = 'docker-compose.override.yml';
    }

    $platform = strtolower(php_uname('s'));
    if (str_contains($platform, 'darwin')) {
        $data['macos'] = true;
    } elseif (\in_array($platform, ['win32', 'win64', 'windows nt'])) {
        $data['power_shell'] = true;
    }

    //                                                   2³² - 1
    if (false === $data['user_id'] || $data['user_id'] > 4294967295) {
        $data['user_id'] = 1000;
    }

    if (0 === $data['user_id']) {
        log('Running as root? Fallback to fake user id.', 'warning');
        $data['user_id'] = 1000;
    }

    return new Context(
        $data,
        pty: Process::isPtySupported(),
        environment: [
            'BUILDKIT_PROGRESS' => 'plain',
        ]
    );
}

#[AsContext(name: 'test')]
function create_test_context(): Context
{
    $c = create_default_context();

    return $c
        ->withEnvironment([
            'APP_ENV' => 'test',
        ])
    ;
}

#[AsContext(name: 'ci')]
function create_ci_context(): Context
{
    $c = create_test_context();

    return $c
        ->withData(
            // override the default context here
            [
                'docker_compose_files' => [
                    'docker-compose.yml',
                    // Usually, the following service is not be needed in the CI
                    'docker-compose.dev.yml',
                    // 'docker-compose.ci.yml',
                ],
            ],
            recursive: false
        )
        ->withEnvironment([
            'COMPOSE_ANSI' => 'never',
        ])
    ;
}


================================================
FILE: .castor/database.php
================================================
<?php

use Castor\Attribute\AsTask;

use function Castor\context;
use function Castor\io;
use function docker\docker_compose;

#[AsTask(description: 'Connect to the PostgreSQL database', name: 'db:client', aliases: ['postgres', 'pg'])]
function postgres_client(): void
{
    io()->title('Connecting to the PostgreSQL database');

    docker_compose(['exec', 'postgres', 'psql', '-U', 'app', 'app'], context()->toInteractive());
}


================================================
FILE: .castor/docker.php
================================================
<?php

namespace docker;

use Castor\Attribute\AsOption;
use Castor\Attribute\AsRawTokens;
use Castor\Attribute\AsTask;
use Castor\Context;
use Castor\Helper\PathHelper;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\Exception\ExceptionInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface as HttpExceptionInterface;

use function Castor\capture;
use function Castor\context;
use function Castor\finder;
use function Castor\fs;
use function Castor\http_client;
use function Castor\io;
use function Castor\open;
use function Castor\run;
use function Castor\variable;

#[AsTask(description: 'Displays some help and available urls for the current project', namespace: '')]
function about(): void
{
    io()->title('About this project');

    io()->comment('Run <comment>castor</comment> to display all available commands.');
    io()->comment('Run <comment>castor about</comment> to display this project help.');
    io()->comment('Run <comment>castor help [command]</comment> to display Castor help.');

    io()->section('Available URLs for this project:');
    $urls = [variable('root_domain'), ...variable('extra_domains')];

    try {
        $routers = http_client()
            ->request('GET', \sprintf('http://%s:8080/api/http/routers', variable('root_domain')))
            ->toArray()
        ;
        $projectName = variable('project_name');
        foreach ($routers as $router) {
            if (!preg_match("{^{$projectName}-(.*)@docker$}", $router['name'])) {
                continue;
            }
            if ("frontend-{$projectName}" === $router['service']) {
                continue;
            }
            if (!preg_match('{^Host\(`(?P<hosts>.*)`\)$}', $router['rule'], $matches)) {
                continue;
            }
            $hosts = explode('`) || Host(`', $matches['hosts']);
            $urls = [...$urls, ...$hosts];
        }
    } catch (HttpExceptionInterface) {
    }

    io()->listing(array_map(fn ($url) => "https://{$url}", array_unique($urls)));
}

#[AsTask(description: 'Opens the project in your browser', namespace: '', aliases: ['open'])]
function open_project(): void
{
    open('https://' . variable('root_domain'));
}

#[AsTask(description: 'Builds the infrastructure', aliases: ['build'])]
function build(
    #[AsOption(description: 'The service to build (default: all services)', autocomplete: 'docker\get_service_names')]
    ?string $service = null,
    ?string $profile = null,
): void {
    generate_certificates(force: false);

    io()->title('Building infrastructure');

    $command = [];

    $command[] = '--profile';
    if ($profile) {
        $command[] = $profile;
    } else {
        $command[] = '*';
    }

    $command = [
        ...$command,
        'build',
        '--build-arg', 'PHP_VERSION=' . variable('php_version'),
        '--build-arg', 'PROJECT_NAME=' . variable('project_name'),
    ];

    if ($service) {
        $command[] = $service;
    }

    docker_compose($command);
}

/**
 * @param list<string> $profiles
 */
#[AsTask(description: 'Builds and starts the infrastructure', aliases: ['up'])]
function up(
    #[AsOption(description: 'The service to start (default: all services)', autocomplete: 'docker\get_service_names')]
    ?string $service = null,
    #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)]
    array $profiles = [],
): void {
    if (!$service && !$profiles) {
        io()->title('Starting infrastructure');
    }

    $command = ['up', '--detach', '--wait', '--no-build'];

    if ($service) {
        $command[] = $service;
    }

    try {
        docker_compose($command, profiles: $profiles);
    } catch (ExceptionInterface $e) {
        io()->error('An error occurred while starting the infrastructure.');
        io()->note('Did you forget to run "castor docker:build"?');
        io()->note('Or you forget to login to the registry?');

        throw $e;
    }
}

/**
 * @param list<string> $profiles
 */
#[AsTask(description: 'Stops the infrastructure', aliases: ['stop'])]
function stop(
    #[AsOption(description: 'The service to stop (default: all services)', autocomplete: 'docker\get_service_names')]
    ?string $service = null,
    #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)]
    array $profiles = [],
): void {
    if (!$service || !$profiles) {
        io()->title('Stopping infrastructure');
    }

    $command = ['stop'];

    if ($service) {
        $command[] = $service;
    }

    docker_compose($command, profiles: $profiles);
}

/**
 * @param array<string> $params
 */
#[AsTask(description: 'Opens a shell (bash) or proxy any command to the builder container', aliases: ['builder'])]
function builder(#[AsRawTokens] array $params = []): int
{
    if (0 === \count($params)) {
        $params = ['bash'];
    }

    $c = context()
        ->toInteractive()
        ->withEnvironment($_ENV + $_SERVER)
    ;

    return (int) docker_compose_run(implode(' ', $params), c: $c)->getExitCode();
}

/**
 * @param list<string> $profiles
 */
#[AsTask(description: 'Displays infrastructure logs', aliases: ['logs'])]
function logs(
    ?string $service = null,
    #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)]
    array $profiles = [],
): void {
    $command = ['logs', '-f', '--tail', '150'];

    if ($service) {
        $command[] = $service;
    }

    docker_compose($command, c: context()->withTty(), profiles: $profiles);
}

#[AsTask(description: 'Lists containers status', aliases: ['ps'])]
function ps(bool $ports = false): void
{
    $command = [
        'ps',
        '--format', 'table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}\t{{.Command}}',
        '--no-trunc',
    ];

    if ($ports) {
        $command[2] .= '\t{{.Ports}}';
    }

    docker_compose($command, profiles: ['*']);

    if (!$ports) {
        io()->comment('You can use the "--ports" option to display ports.');
    }
}

#[AsTask(description: 'Cleans the infrastructure (remove container, volume, networks)', aliases: ['destroy'])]
function destroy(
    #[AsOption(description: 'Force the destruction without confirmation', shortcut: 'f')]
    bool $force = false,
): void {
    io()->title('Destroying infrastructure');

    if (!$force) {
        io()->warning('This will permanently remove all containers, volumes, networks... created for this project.');
        io()->note('You can use the --force option to avoid this confirmation.');
        if (!io()->confirm('Are you sure?', false)) {
            io()->comment('Aborted.');

            return;
        }
    }

    docker_compose(['down', '--remove-orphans', '--volumes', '--rmi=local'], profiles: ['*']);
    $files = finder()
        ->in(variable('root_dir') . '/infrastructure/docker/services/router/certs/')
        ->name('*.pem')
        ->files()
    ;
    fs()->remove($files);
}

#[AsTask(description: 'Generates SSL certificates (with mkcert if available or self-signed if not)')]
function generate_certificates(
    #[AsOption(description: 'Force the certificates re-generation without confirmation', shortcut: 'f')]
    bool $force = false,
): void {
    $sslDir = variable('root_dir') . '/infrastructure/docker/services/router/certs';

    if (file_exists("{$sslDir}/cert.pem") && !$force) {
        io()->comment('SSL certificates already exists.');
        io()->note('Run "castor docker:generate-certificates --force" to generate new certificates.');

        return;
    }

    io()->title('Generating SSL certificates');

    if ($force) {
        if (file_exists($f = "{$sslDir}/cert.pem")) {
            io()->comment('Removing existing certificates in infrastructure/docker/services/router/certs/*.pem.');
            unlink($f);
        }

        if (file_exists($f = "{$sslDir}/key.pem")) {
            unlink($f);
        }
    }

    $finder = new ExecutableFinder();
    $mkcert = $finder->find('mkcert');

    if ($mkcert) {
        $pathCaRoot = capture(['mkcert', '-CAROOT']);

        if (!is_dir($pathCaRoot)) {
            io()->warning('You must have mkcert CA Root installed on your host with "mkcert -install" command.');

            return;
        }

        $rootDomain = variable('root_domain');

        run([
            'mkcert',
            '-cert-file', "{$sslDir}/cert.pem",
            '-key-file', "{$sslDir}/key.pem",
            $rootDomain,
            "*.{$rootDomain}",
            ...variable('extra_domains'),
        ]);

        io()->success('Successfully generated SSL certificates with mkcert.');

        if ($force) {
            io()->note('Please restart the infrastructure to use the new certificates with "castor up" or "castor start".');
        }

        return;
    }

    run(['infrastructure/docker/services/router/generate-ssl.sh'], context: context()->withQuiet());

    io()->success('Successfully generated self-signed SSL certificates in infrastructure/docker/services/router/certs/*.pem.');
    io()->comment('Consider installing mkcert to generate locally trusted SSL certificates and run "castor docker:generate-certificates --force".');

    if ($force) {
        io()->note('Please restart the infrastructure to use the new certificates with "castor up" or "castor start".');
    }
}

#[AsTask(description: 'Starts the workers', namespace: 'docker:worker', name: 'start', aliases: ['start-workers'])]
function workers_start(): void
{
    io()->title('Starting workers');

    $command = ['up', '--detach', '--wait', '--no-build'];
    $profiles = ['worker', 'default'];

    try {
        docker_compose($command, profiles: $profiles);
    } catch (ProcessFailedException $e) {
        preg_match('/service "(\w+)" depends on undefined service "(\w+)"/', $e->getProcess()->getErrorOutput(), $matches);
        if (!$matches) {
            throw $e;
        }

        $r = new \ReflectionFunction(__FUNCTION__);

        io()->newLine();
        io()->error('An error occurred while starting the workers.');
        io()->warning(\sprintf(
            <<<'EOT'
                The "%1$s" service depends on the "%2$s" service, which is not defined in the current docker-compose configuration.

                Usually, this means that the service "%2$s" is not defined in the same profile (%3$s) as the "%1$s" service.

                You can try to add its profile in the current task: %4$s:%5$s
                EOT,
            $matches[1],
            $matches[2],
            implode(', ', $profiles),
            PathHelper::makeRelative((string) $r->getFileName()),
            $r->getStartLine(),
        ));
    }
}

#[AsTask(description: 'Stops the workers', namespace: 'docker:worker', name: 'stop', aliases: ['stop-workers'])]
function workers_stop(): void
{
    io()->title('Stopping workers');

    // Docker compose cannot stop a single service in a profile, if it depends
    // on another service in another profile. To make it work, we need to select
    // both profiles, and so stop both services

    // So we find all services, in all profiles, and manually filter the one
    // that has the "worker" profile, then we stop it
    $command = ['stop'];

    foreach (get_services() as $name => $service) {
        foreach ($service['profiles'] ?? [] as $profile) {
            if ('worker' === $profile) {
                $command[] = $name;

                continue 2;
            }
        }
    }

    docker_compose($command, profiles: ['*']);
}

/**
 * @param list<string> $subCommand
 * @param list<string> $profiles
 */
function docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process
{
    $c ??= context();
    $profiles = $profiles ?: ['default'];

    $domains = [$c['root_domain'], ...$c['extra_domains']];
    $domains = '`' . implode('`) || Host(`', $domains) . '`';

    $c = $c->withEnvironment([
        'PROJECT_NAME' => $c['project_name'],
        'PROJECT_ROOT_DOMAIN' => $c['root_domain'],
        'PROJECT_DOMAINS' => $domains,
        'USER_ID' => $c['user_id'],
        'PHP_VERSION' => $c['php_version'],
        'REGISTRY' => $c['registry'] ?? '',
    ]);

    if ($c['APP_ENV'] ?? false) {
        $c = $c->withEnvironment([
            'APP_ENV' => $c['APP_ENV'] ?? '',
        ]);
    }

    $command = [
        'docker',
        'compose',
        '-p', $c['project_name'],
    ];
    foreach ($profiles as $profile) {
        $command[] = '--profile';
        $command[] = $profile;
    }

    foreach ($c['docker_compose_files'] as $file) {
        $command[] = '-f';
        $command[] = $c['root_dir'] . '/infrastructure/docker/' . $file;
    }

    $command = array_merge($command, $subCommand);

    return run($command, context: $c);
}

function docker_compose_run(
    string $runCommand,
    ?Context $c = null,
    string $service = 'builder',
    bool $noDeps = true,
    ?string $workDir = null,
    bool $portMapping = false,
): Process {
    $c ??= context();

    $command = [
        'run',
        '--rm',
    ];

    if ($noDeps) {
        $command[] = '--no-deps';
    }

    if ($portMapping) {
        $command[] = '--service-ports';
    }

    if (null !== $workDir) {
        $command[] = '-w';
        $command[] = $workDir;
    }

    foreach ($c['docker_compose_run_environment'] as $key => $value) {
        $command[] = '-e';
        $command[] = "{$key}={$value}";
    }

    $command[] = $service;
    $command[] = '/bin/bash';
    $command[] = '-c';
    $command[] = "{$runCommand}";

    return docker_compose($command, c: $c, profiles: ['*']);
}

function docker_exit_code(
    string $runCommand,
    ?Context $c = null,
    string $service = 'builder',
    bool $noDeps = true,
    ?string $workDir = null,
): int {
    $c = ($c ?? context())->withAllowFailure();

    $process = docker_compose_run(
        runCommand: $runCommand,
        c: $c,
        service: $service,
        noDeps: $noDeps,
        workDir: $workDir,
    );

    return $process->getExitCode() ?? 0;
}

// Mac users have a lot of problems running Yarn / Webpack on the Docker stack
// so this func allow them to run these tools on their host
function run_in_docker_or_locally_for_mac(string $command, ?Context $c = null): void
{
    $c ??= context();

    if ($c['macos']) {
        run($command, context: $c->withWorkingDirectory($c['root_dir']));
    } else {
        docker_compose_run($command, c: $c);
    }
}

#[AsTask(description: 'Push images cache to the registry', namespace: 'docker', name: 'push', aliases: ['push'])]
function push(bool $dryRun = false): void
{
    $registry = variable('registry');

    if (!$registry) {
        throw new \RuntimeException('You must define a registry to push images.');
    }

    // Generate bake file
    $targets = [];

    foreach (get_services() as $service => $config) {
        $cacheFrom = $config['build']['cache_from'][0] ?? null;

        if (null === $cacheFrom) {
            continue;
        }

        $cacheFrom = explode(',', $cacheFrom);
        $reference = null;
        $type = null;

        if (1 === \count($cacheFrom)) {
            $reference = $cacheFrom[0];
            $type = 'registry';
        } else {
            foreach ($cacheFrom as $part) {
                $from = explode('=', $part);

                if (2 !== \count($from)) {
                    continue;
                }

                if ('type' === $from[0]) {
                    $type = $from[1];
                }

                if ('ref' === $from[0]) {
                    $reference = $from[1];
                }
            }
        }

        $targets[] = [
            'reference' => $reference,
            'type' => $type,
            'context' => $config['build']['context'],
            'dockerfile' => $config['build']['dockerfile'] ?? 'Dockerfile',
            'target' => $config['build']['target'] ?? null,
        ];
    }

    $content = \sprintf(<<<'EOHCL'
        group "default" {
            targets = [%s]
        }

        EOHCL
        , implode(', ', array_map(fn ($target) => \sprintf('"%s"', $target['target']), $targets)));

    foreach ($targets as $target) {
        $content .= \sprintf(<<<'EOHCL'
            target "%s" {
                context    = "%s"
                dockerfile = "%s"
                cache-from = ["%s"]
                cache-to   = ["type=%s,ref=%s,mode=max"]
                target     = "%s"
                args = {
                    PHP_VERSION = "%s"
                }
            }

            EOHCL
            , $target['target'], $target['context'], $target['dockerfile'], $target['reference'], $target['type'], $target['reference'], $target['target'], variable('php_version'));
    }

    if ($dryRun) {
        io()->write($content);

        return;
    }

    // write bake file in tmp file
    $bakeFile = tempnam(sys_get_temp_dir(), 'bake');
    file_put_contents($bakeFile, $content);

    // Run bake
    run(['docker', 'buildx', 'bake', '-f', $bakeFile]);
}

/**
 * @return array<string, array{profiles?: list<string>, build: array{context: string, dockerfile?: string, cache_from?: list<string>, target?: string}}>
 */
function get_services(): array
{
    return json_decode(
        docker_compose(
            ['config', '--format', 'json'],
            context()->withQuiet(),
            profiles: ['*'],
        )->getOutput(),
        true,
    )['services'];
}

/**
 * @return string[]
 */
function get_service_names(): array
{
    return array_keys(get_services());
}


================================================
FILE: .castor/init.php
================================================
<?php

use Castor\Attribute\AsTask;

use function Castor\fs;
use function Castor\variable;
use function docker\build;
use function docker\docker_compose_run;

#[AsTask(description: 'Initialize the project')]
function init(): void
{
    fs()->remove([
        '.github/',
        'README.md',
        'CHANGELOG.md',
        'CONTRIBUTING.md',
        'LICENSE',
        __FILE__,
    ]);
    fs()->rename('README.dist.md', 'README.md');

    $readMeContent = file_get_contents('README.md');

    if (false === $readMeContent) {
        return;
    }

    $urls = [variable('root_domain'), ...variable('extra_domains')];
    $readMeContent = str_replace('<your hostnames>', implode(' ', $urls), $readMeContent);
    file_put_contents('README.md', $readMeContent);
}

#[AsTask(description: 'Install Symfony')]
function symfony(bool $webApp = false): void
{
    $base = rtrim(variable('root_dir') . '/application');

    $gitIgnore = $base . '/.gitignore';
    $gitIgnoreContent = '';
    if (file_exists($gitIgnore)) {
        $gitIgnoreContent = file_get_contents($gitIgnore);
    }

    build();
    docker_compose_run('composer create-project symfony/skeleton sf');

    fs()->mirror($base . '/sf/', $base);
    fs()->remove([$base . '/sf', $base . '/var']);

    if ($webApp) {
        docker_compose_run('composer require webapp');
    }

    docker_compose_run("sed -i 's#^DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\\?serverVersion=16\\&charset=utf8#' .env");
    file_put_contents($gitIgnore, $gitIgnoreContent, \FILE_APPEND);
}


================================================
FILE: .castor/qa.php
================================================
<?php

namespace qa;

// use Castor\Attribute\AsRawTokens;
use Castor\Attribute\AsOption;
use Castor\Attribute\AsTask;

use function Castor\io;
use function Castor\variable;
use function docker\docker_compose_run;
use function docker\docker_exit_code;

#[AsTask(description: 'Runs all QA tasks')]
function all(): int
{
    $cs = cs();
    $phpstan = phpstan();
    $twigCs = twigCs();
    // $phpunit = phpunit();

    return max($cs, $phpstan, $twigCs/* , $phpunit */);
}

#[AsTask(description: 'Installs tooling')]
function install(): void
{
    io()->title('Installing QA tooling');

    docker_compose_run('composer install -o', workDir: '/var/www/tools/php-cs-fixer');
    docker_compose_run('composer install -o', workDir: '/var/www/tools/phpstan');
    docker_compose_run('composer install -o', workDir: '/var/www/tools/twig-cs-fixer');
}

#[AsTask(description: 'Updates tooling')]
function update(): void
{
    io()->title('Updating QA tooling');

    docker_compose_run('composer update -o', workDir: '/var/www/tools/php-cs-fixer');
    docker_compose_run('composer update -o', workDir: '/var/www/tools/phpstan');
    docker_compose_run('composer update -o', workDir: '/var/www/tools/twig-cs-fixer');
}

// /**
//  * @param string[] $rawTokens
//  */
// #[AsTask(description: 'Runs PHPUnit', aliases: ['phpunit'])]
// function phpunit(#[AsRawTokens] array $rawTokens = []): int
// {
//     io()->section('Running PHPUnit...');
//
//     return docker_exit_code('bin/phpunit ' . implode(' ', $rawTokens));
// }

#[AsTask(description: 'Runs PHPStan', aliases: ['phpstan'])]
function phpstan(
    #[AsOption(description: 'Generate baseline file', shortcut: 'b')]
    bool $baseline = false,
): int {
    if (!is_dir(variable('root_dir') . '/tools/phpstan/vendor')) {
        install();
    }

    io()->section('Running PHPStan...');

    $options = $baseline ? '--generate-baseline --allow-empty-baseline' : '';
    $command = \sprintf('phpstan analyse --memory-limit=-1 %s -v', $options);

    return docker_exit_code($command, workDir: '/var/www');
}

#[AsTask(description: 'Fixes Coding Style', aliases: ['cs'])]
function cs(bool $dryRun = false): int
{
    if (!is_dir(variable('root_dir') . '/tools/php-cs-fixer/vendor')) {
        install();
    }

    io()->section('Running PHP CS Fixer...');

    if ($dryRun) {
        return docker_exit_code('php-cs-fixer fix --dry-run --diff', workDir: '/var/www');
    }

    return docker_exit_code('php-cs-fixer fix', workDir: '/var/www');
}

#[AsTask(description: 'Fixes Twig Coding Style', aliases: ['twig-cs'])]
function twigCs(bool $dryRun = false): int
{
    if (!is_dir(variable('root_dir') . '/tools/twig-cs-fixer/vendor')) {
        install();
    }

    io()->section('Running Twig CS Fixer...');

    if ($dryRun) {
        return docker_exit_code('twig-cs-fixer', workDir: '/var/www');
    }

    return docker_exit_code('twig-cs-fixer --fix', workDir: '/var/www');
}


================================================
FILE: .gitattributes
================================================
# Force LF line ending (mandatory for Windows)
* text=auto eol=lf


================================================
FILE: .github/workflows/cache.yml
================================================
name: Push docker image to registry

"on":
  push:
    # Only run this job when pushing to the main branch
    branches: ["main"]

permissions:
  contents: read
  packages: write

env:
  DS_REGISTRY: "ghcr.io/jolicode/docker-starter"
  DS_PHP_VERSION: "8.5"

jobs:
  push-images:
    name: Push image to registry
    runs-on: ubuntu-latest
    steps:
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Log in to registry
        shell: bash
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin

      - name: setup-castor
        uses: castor-php/setup-castor@v1.0.0

      - uses: actions/checkout@v6


      - name: "Build and start the infrastructure"
        run: "castor docker:push"


================================================
FILE: .github/workflows/ci.yml
================================================
name: Continuous Integration

"on":
    push:
        branches: ["main"]
    pull_request:
        branches: ["main"]
    schedule:
        - cron: "0 0 * * MON"

permissions:
    contents: read
    packages: read

env:
    # Fix for symfony/color detection. We know GitHub Actions can handle it
    ANSICON: 1
    CASTOR_CONTEXT: ci
    DS_REGISTRY: "ghcr.io/jolicode/docker-starter"

jobs:
    check-dockerfiles:
        name: Check Dockerfile
        runs-on: ubuntu-latest
        steps:
            - name: Checkout
              uses: actions/checkout@v6

            - name: Check php/Dockerfile
              uses: hadolint/hadolint-action@v3.3.0
              with:
                  dockerfile: infrastructure/docker/services/php/Dockerfile

    ci:
        name: Test with PHP ${{ matrix.php-version }}
        strategy:
            fail-fast: false
            matrix:
                php-version: ["8.3", "8.4", "8.5"]
        runs-on: ubuntu-latest
        env:
            DS_PHP_VERSION: ${{ matrix.php-version }}
        steps:
            - name: Set up Docker Buildx
              uses: docker/setup-buildx-action@v4

            - name: Log in to registry
              shell: bash
              run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin

            - name: setup-castor
              uses: castor-php/setup-castor@v1.0.0

            - uses: actions/checkout@v6

            - name: "Build and start the infrastructure"
              run: "castor start"

            - name: "Check PHP coding standards"
              run: "castor qa:cs --dry-run"

            - name: "Run PHPStan"
              run: "castor qa:phpstan"

            - name: "Test HTTP server"
              run: |
                  set -e
                  set -o pipefail

                  curl --fail --insecure --silent -H "Host: app.test" https://127.0.0.1 | grep "Hello world"
                  curl --fail --insecure --silent -H "Host: app.test" https://127.0.0.1 | grep "${{ matrix.php-version }}"

            - name: "Test builder"
              run: |
                  set -e
                  set -o pipefail

                  cat > .castor/test.php <<'EOPHP'
                  <?php

                  use Castor\Attribute\AsTask;
                  use function docker\docker_compose_run;

                  #[AsTask()]
                  function test()
                  {
                      docker_compose_run('echo "Hello World"');
                  }

                  #[AsTask()]
                  function app_env()
                  {
                      docker_compose_run('php public/index.php');
                  }
                  EOPHP

                  castor test | grep "Hello World"
                  CASTOR_CONTEXT=default castor app-env | grep 'Environment: not set'"
                  CASTOR_CONTEXT=test castor app-env | grep 'Environment: test'"

            - name: "Test communication with DB"
              run: |
                  set -e
                  set -o pipefail

                  cat > application/public/index.php <<'EOPHP'
                  <?php
                  $pdo = new PDO('pgsql:host=postgres;dbname=app', 'app', 'app');
                  $pdo->exec('CREATE TABLE test (id integer NOT NULL)');
                  $pdo->exec('INSERT INTO test VALUES (1)');
                  echo $pdo->query('SELECT * from test')->fetchAll() ? 'database OK' : 'database KO';
                  EOPHP

                  # FPM seems super slow to detect the change, we need to wait a bit
                  sleep 3

                  curl --fail --insecure --silent -H "Host: app.test" https://127.0.0.1 | grep "database OK"


================================================
FILE: .gitignore
================================================
/.castor.stub.php
/infrastructure/docker/docker-compose.override.yml
/infrastructure/docker/services/router/certs/*.pem

# Tools
.php-cs-fixer.cache
.twig-cs-fixer.cache


================================================
FILE: .home/.gitignore
================================================
/*
!.gitignore


================================================
FILE: .php-cs-fixer.php
================================================
<?php

$finder = PhpCsFixer\Finder::create()
    ->ignoreVCSIgnored(true)
    ->ignoreDotFiles(false)
    ->in(__DIR__)
    ->append([
        __FILE__,
    ])
;

return (new PhpCsFixer\Config())
    ->setUnsupportedPhpVersionAllowed(true)
    ->setRiskyAllowed(true)
    ->setRules([
        '@PHP83Migration' => true,
        '@PhpCsFixer' => true,
        '@Symfony' => true,
        '@Symfony:risky' => true,
        'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it
        'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it
        'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it
        'concat_space' => ['spacing' => 'one'],
        'ordered_class_elements' => true, // Symfony(PSR12) override the default value, but we don't want
        'blank_line_before_statement' => true, // Symfony(PSR12) override the default value, but we don't want
    ])
    ->setFinder($finder)
;


================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG

## 4.0.0 (not released yet)

* Tooling
  * Migrate from Invoke to Castor
  * Add `castor symfony` to install a Symfony application
  * Add `castor init` command to initialize a new project
  * Add a `test` context
* Services
  * Upgrade Traefik from v2.7 to v3.0
  * Upgrade to PostgreSQL v16
  * Replace maildev with Mailpit
* PHP
  * Drop support for PHP < 8.3
  * Add support for PHP 8.4, 8.5
  * Add support for pie
  * Add some PHP tooling (PHP-CS-Fixer, PHPStan, Twig-CS-Fixer)
  * Update Composer to version 2.8
* Builder
  * Update NodeJS to version 20.x LTS
  * Add support for autocomplete (composer & symfony)
* Docker:
  * Add a dockerfile linter
  * Do not store certificates in the router image
  * Do not hardcode a user in the Dockerfile (and so the image), map it dynamically
  * Mount the project in `/var/www` instead of `/home/app`
  * Add support for caching image cache in a registry
  * Upgrade base to Debian Bookworm (12.5)

## 3.11.0 (2023-05-30)

* Use docker stages to build images

## 3.10.0 (2023-05-22)

* Fix workers detection in docker v23
* Update Invoke to version 2.1
* Update Composer to version 2.5.5
* Upgrade NodeJS to 18.x LTS version
* Migrate to Compose V2

## 3.9.0 (2022-12-21)

* Update documentation cookbook for installing redirection.io and Blackfire to
  remove deprecated apt-key usage
* Update Composer to version 2.5.0
* Increase the number of FPM worker from 4 to 25
* Enabled PHP FPM status page on `/php-fpm-status`
* Added support for PHP 8.2

## 3.8.0 (2022-06-15)

* Add documentation cookbook for using pg_activity
* Forward CI env vars in Docker containers
* Run the npm/yarn/webpack commands on the host for all mac users (even the ones not using Dinghy)
* Tests with PHP 7.4, 8.0, and 8.1

## 3.7.0 (2022-05-24)

* Add documentation cookbook for installing redirection.io
* Upgrade to Traefik v2.7.0
* Upgrade to PostgreSQL v14
* Upgrade to Composer v2.3

## 3.6.0 (2022-03-10)

* Upgrade NodeJS to version 16.x LTS and remove deprecated apt-key usage
* Various fix in the documentation
* Remove certificates when destroying infrastructure

## 3.5.0 (2022-01-27)

* Update PHP to version 8.1
* Generate SSL certificates with mkcert when available (self-signed otherwise)

## 3.4.0 (2021-10-13)

* Fix `COMPOSER_CACHE_DIR` default value when composer is not installed on host
* Upgrade base to Debian Bullseye (11.0)
* Document webpack 5+ integration

## 3.3.0 (2021-06-03)

* Update PHP to version 8.0
* Update Composer to version 2.1.0
* Fix APT key for Sury repository
* Fix the version of our debian base image

## 3.2.0 (2021-02-17)

* Migrate CI from Circle to GitHub Actions
* Add support for `docker-compose.override.yml`

## 3.1.0 (2020-11-13)

 * Fix TTY on all OS
 * Add default vendor installation command with auto-detection in `install()` for Yarn, NPM and Composer
 * Update Composer to version 2
 * Install by default php-uuid extension
 * Update NodeJS from 12.x to 14.x

## 3.0.0 (2020-07-01)

 * Migrate from Fabric to Invoke
 * Migrate from Alpine to Debian for PHP images
 * Add a confirmation when calling `inv destroy`
 * Tweak the PHP configuration
 * Upgrade PostgreSQL from 11 to 12
 * Upgrade Traefik from 2.0 to 2.2
 * Add an `help` task. This is the default one
 * The help command list all HTTP(s) services available
 * The help command list tasks available
 * Fix the support for Mac and Windows
 * Try to map the correct Composer cache dir from the host
 * Enhance the documentation

## 2.0.0 (2020-01-08)

* Better Docker for Windows support
* Add support for running many projects at the same time
* Upgrade Traefik from 1.7 to 2.0
* Add native support for workers

## 1.0.0 (2019-07-27)

* First release


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

First of all, **thank you** for contributing, **you are awesome**!

Everybody should be able to help. Here's how you can do it:

1. [Fork it](https://github.com/jolicode/docker-starter/fork_select)
2. improve it
3. submit a [pull request](https://help.github.com/articles/creating-a-pull-request)

Here's some tips to make you the best contributor ever:

* [Rules](#rules)
* [Keeping your fork up-to-date](#keeping-your-fork-up-to-date)

## Rules

Here are a few rules to follow in order to ease code reviews, and discussions
before maintainers accept and merge your work.

Please, write [commit messages that make
sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
before submitting your Pull Request (see also how to [keep your
fork up-to-date](#keeping-your-fork-up-to-date)).

One may ask you to [squash your
commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
too. This is used to "clean" your Pull Request before merging it (we don't want
commits such as `fix tests`, `fix 2`, `fix 3`, etc.).

Also, while creating your Pull Request on GitHub, you MUST write a description
which gives the context and/or explains why you are creating it.

Your work will then be reviewed as soon as possible (suggestions about some
changes, improvements or alternatives may be given).

## Keeping your fork up-to-date

To keep your fork up-to-date, you should track the upstream (original) one
using the following command:


```shell
git remote add upstream https://github.com/jolicode/docker-starter.git
```

Then get the upstream changes:

```shell
git checkout master
git pull --rebase upstream master
git checkout <your-branch>
git rebase master
```

Finally, publish your changes:

```shell
git push -f origin <your-branch>
```

Your pull request will be automatically updated.

Thank you!


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019-present JoliCode

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.dist.md
================================================
# My project

## Running the application locally

### Requirements

A Docker environment is provided and requires you to have these tools available:

 * Docker
 * Bash
 * [Castor](https://github.com/jolicode/castor#installation)

#### Castor

Once `castor` is installed, in order to improve your usage of `castor` scripts, you
can install console autocompletion script.

If you are using bash:

```bash
castor completion | sudo tee /etc/bash_completion.d/castor
```

If you are using something else, please refer to your shell documentation. You
may need to use `castor completion > /to/somewhere`.

`castor` supports completion for `bash`, `zsh` & `fish` shells.

### Docker environment

The Docker infrastructure provides a web stack with:
 - NGINX
 - PostgreSQL
 - PHP
 - Traefik
 - A container with some tooling:
   - Composer
   - Node
   - Yarn / NPM

### Domain configuration (first time only)

Before running the application for the first time, ensure your domain names
point the IP of your Docker daemon by editing your `/etc/hosts` file.

This IP is probably `127.0.0.1` unless you run Docker in a special VM (like docker-machine for example).

> [!NOTE]
> The router binds port 80 and 443, that's why it will work with `127.0.0.1`

```
echo '127.0.0.1 <your hostnames>' | sudo tee -a /etc/hosts
```

### Starting the stack

Launch the stack by running this command:

```bash
castor start
```

> [!NOTE]
> the first start of the stack should take a few minutes.

The site is now accessible at the hostnames you have configured over HTTPS
(you may need to accept self-signed SSL certificate if you do not have `mkcert`
installed on your computer - see below).

### SSL certificates

HTTPS is supported out of the box. SSL certificates are not versioned and will
be generated the first time you start the infrastructure (`castor start`) or if
you run `castor docker:generate-certificates`.

If you have `mkcert` installed on your computer, it will be used to generate
locally trusted certificates. See [`mkcert` documentation](https://github.com/FiloSottile/mkcert#installation)
to understand how to install it. Do not forget to install CA root from `mkcert`
by running `mkcert -install`.

If you don't have `mkcert`, then self-signed certificates will instead be
generated with `openssl`. You can configure [infrastructure/docker/services/router/openssl.cnf](infrastructure/docker/services/router/openssl.cnf)
to tweak certificates.

You can run `castor docker:generate-certificates --force` to recreate new certificates
if some were already generated. Remember to restart the infrastructure to make
use of the new certificates with `castor build && castor up` or `castor start`.

### Builder

Having some composer, yarn or other modifications to make on the project?
Start the builder which will give you access to a container with all these
tools available:

```bash
castor builder
```

### Other tasks

Checkout `castor` to have the list of available tasks.


================================================
FILE: README.md
================================================
<h1 align="center">
  <a href="https://github.com/jolicode/docker-starter"><img src="https://jolicode.com/media/original/oss/headers/docker-starter.png" alt="Docker Starter"></a>
  <br />
  Docker Starter
  <br />
  <sub><em><h6>A docker-based infrastructure wrapped in an easy-to-use command line, oriented for PHP projects.</h6></em></sub>
</h1>

This repository contains a collection of Dockerfile and docker-compose configurations
for your PHP projects with built-in support for HTTPS, custom domain, databases, workers...
and is used as a foundation for our projects at [JoliCode](https://jolicode.com/).

> [!WARNING]
> You are reading the README of version 4 that uses [Castor](https://castor.jolicode.com).

* If you are using [Invoke](https://www.pyinvoke.org/), you can read the [dedicated README](https://github.com/jolicode/docker-starter/tree/v3.11.0);
* If you are using [Fabric](https://www.fabfile.org/), you can read the [dedicated README](https://github.com/jolicode/docker-starter/tree/v2.0.0);

## Project configuration

Before executing any command, you need to configure a few parameters in the
`castor.php` file, in the `create_default_variables()` function:

* `project_name` (**required**): This will be used to prefix all docker objects
(network, images, containers);

* `root_domain` (optional, default: `project_name + '.test'`): This is the root
domain where the application will be available;

* `extra_domains` (optional): This contains extra domains where the application
will be available;

* `php_version` (optional, default: `8.5`): This is PHP version.

For example:

```php
function create_default_variables(): array
{
    $projectName = 'app';
    $tld = 'test';

    return [
        'project_name' => $projectName,
        'root_domain' => "{$projectName}.{$tld}",
        'extra_domains' => [
            "www.{$projectName}.{$tld}",
            "admin.{$projectName}.{$tld}",
            "api.{$projectName}.{$tld}",
        ],
        'php_version' => 8.3,
    ];
)
```

Will give you `https://app.test`,  `https://www.app.test`,
`https://api.app.test` and `https://admin.app.test` pointing at your
`application/` directory.

> [!NOTE]
> Some castor tasks have been added for DX purposes. Checkout and adapt
> the tasks `install`, `migrate` and `cache_clear` to your project.

## Usage documentation

We provide a [README.dist.md](./README.dist.md) to bootstrap your project
documentation, with everything you need to know to start and interact with the
infrastructure.

If you want to install a Symfony project, you can run (before `castor init`):

```
castor symfony [--web-app]
```

To replace this README with the dist, and remove all unnecessary files, you can
run:

```bash
castor init
```

> [!NOTE]
> This command can be run only once

Also, in order to improve your usage of castor scripts, you can install console
autocompletion script.

If you are using bash:

```bash
castor completion | sudo tee /etc/bash_completion.d/castor
```

If you are using something else, please refer to your shell documentation. You
may need to use `castor completion > /to/somewhere`.

Castor supports completion for `bash`, `zsh` & `fish` shells.

## Cookbooks

### How to install third party tools with Composer

<details>

<summary>Read the cookbook</summary>

If you want to install some third party tools with Composer, it is recommended to install them in their dedicated directory.
PHPStan and PHP-CS-Fixer are already installed in the `tools` directory.

We suggest to:

1. create a composer.json which requires only this tool in `tools/<tool name>/composer.json`;

1. 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>`;

> [!NOTE]
> Relative symlinks works here, because the first part of the command is relative to the second part, not to the current directory.

Since `tools/bin` path is appended to the `$PATH`, tools will be available globally in the builder container.

</details>

### How to change the layout of the project

<details>

<summary>Read the cookbook</summary>

If you want to rename the `application` directory, or even move its content to
the root directory, you have to edit each reference to it. Theses references
represent each application entry point, whether it be over HTTP or CLI.
Usually, there is three places where you need to do it:

* In Nginx configuration file:
  `infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf`. You need
  to update  `http.server.root` option to the new path. For example:
  ```diff
  - root /var/www/application/public;
  + root /var/www/public;
  ```
* In all workers configuration file:
  `infrastructure/docker/docker-compose.worker.yml`:
  ```diff
  - command: php -d memory_limit=1G /var/www/application/bin/console messenger:consume async --memory-limit=128M
  + command: php -d memory_limit=1G /var/www/bin/console messenger:consume async --memory-limit=128M
  ```
* In the builder, to land in the right directory directly:
  `infrastructure/docker/services/php/Dockerfile`:
  ```diff
  - WORKDIR /var/www/application
  + WORKDIR /var/www
  ```

</details>

### How to use MariaDB instead of PostgreSQL

<details>

<summary>Read the cookbook</summary>

In order to use MariaDB, you will need to apply this patch:

```diff
diff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml
index d00f315..bdfdc65 100644
--- a/infrastructure/docker/docker-compose.builder.yml
+++ b/infrastructure/docker/docker-compose.builder.yml
@@ -10,7 +10,7 @@ services:
     builder:
         build: services/builder
         depends_on:
-            - postgres
+            - mariadb
         environment:
             - COMPOSER_MEMORY_LIMIT=-1
         volumes:
diff --git a/infrastructure/docker/docker-compose.worker.yml b/infrastructure/docker/docker-compose.worker.yml
index 2eda814..59f8fed 100644
--- a/infrastructure/docker/docker-compose.worker.yml
+++ b/infrastructure/docker/docker-compose.worker.yml
@@ -5,7 +5,7 @@ x-services-templates:
     worker_base: &worker_base
         build: services/worker
         depends_on:
-            - postgres
+            - mariadb
             #- rabbitmq
         volumes:
             - "../..:/var/www:cached"
diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml
index 49a2661..1804a01 100644
--- a/infrastructure/docker/docker-compose.yml
+++ b/infrastructure/docker/docker-compose.yml
@@ -1,7 +1,7 @@
 version: '3.7'

 volumes:
-    postgres-data: {}
+    mariadb-data: {}

 services:
     router:
@@ -13,7 +13,7 @@ services:
     frontend:
         build: services/frontend
         depends_on:
-            - postgres
+            - mariadb
         volumes:
             - "../..:/var/www:cached"
         labels:
@@ -24,10 +24,7 @@ services:
             # Comment the next line to be able to access frontend via HTTP instead of HTTPS
             - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file"

-    postgres:
-        image: postgres:16
-        environment:
-            - POSTGRES_USER=app
-            - POSTGRES_PASSWORD=app
+    mariadb:
+        image: mariadb:11
+        environment:
+            - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1
+        healthcheck:
+            test: "mariadb-admin ping -h localhost"
+            interval: 5s
+            timeout: 5s
+            retries: 10
         volumes:
-            - postgres-data:/var/lib/postgresql/data
+            - mariadb-data:/var/lib/mysql
diff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile
index 56e1835..95fee78 100644
--- a/infrastructure/docker/services/php/Dockerfile
+++ b/infrastructure/docker/services/php/Dockerfile
@@ -24,7 +24,7 @@ RUN apk add --no-cache \
     php${PHP_VERSION}-intl \
     php${PHP_VERSION}-mbstring \
-    php${PHP_VERSION}-pgsql \
+    php${PHP_VERSION}-mysql \
     php${PHP_VERSION}-xml \
     php${PHP_VERSION}-zip \
```

</details>

### How to use MySQL instead of PostgreSQL

<details>

<summary>Read the cookbook</summary>

In order to use MySQL, you will need to apply this patch:

```diff
diff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml
index d00f315..bdfdc65 100644
--- a/infrastructure/docker/docker-compose.builder.yml
+++ b/infrastructure/docker/docker-compose.builder.yml
@@ -10,7 +10,7 @@ services:
     builder:
         build: services/builder
         depends_on:
-            - postgres
+            - mysql
         environment:
             - COMPOSER_MEMORY_LIMIT=-1
         volumes:
diff --git a/infrastructure/docker/docker-compose.worker.yml b/infrastructure/docker/docker-compose.worker.yml
index 2eda814..59f8fed 100644
--- a/infrastructure/docker/docker-compose.worker.yml
+++ b/infrastructure/docker/docker-compose.worker.yml
@@ -5,7 +5,7 @@ x-services-templates:
     worker_base: &worker_base
         build: services/worker
         depends_on:
-            - postgres
+            - mysql
             #- rabbitmq
         volumes:
             - "../..:/var/www:cached"
diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml
index 49a2661..1804a01 100644
--- a/infrastructure/docker/docker-compose.yml
+++ b/infrastructure/docker/docker-compose.yml
@@ -1,7 +1,7 @@
 version: '3.7'

 volumes:
-    postgres-data: {}
+    mysql-data: {}

 services:
     router:
@@ -13,7 +13,7 @@ services:
     frontend:
         build: services/frontend
         depends_on:
-            - postgres
+            - mysql
         volumes:
             - "../..:/var/www:cached"
         labels:
@@ -24,10 +24,7 @@ services:
             # Comment the next line to be able to access frontend via HTTP instead of HTTPS
             - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file"

-    postgres:
-        image: postgres:16
-        environment:
-            - POSTGRES_USER=app
-            - POSTGRES_PASSWORD=app
+    mysql:
+        image: mysql:8
+        environment:
+            - MYSQL_ALLOW_EMPTY_PASSWORD=1
+        healthcheck:
+            test: "mysqladmin ping -h localhost"
+            interval: 5s
+            timeout: 5s
+            retries: 10
         volumes:
-            - postgres-data:/var/lib/postgresql/data
+            - mysql-data:/var/lib/mysql
diff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile
index 56e1835..95fee78 100644
--- a/infrastructure/docker/services/php/Dockerfile
+++ b/infrastructure/docker/services/php/Dockerfile
@@ -24,7 +24,7 @@ RUN apk add --no-cache \
     php${PHP_VERSION}-intl \
     php${PHP_VERSION}-mbstring \
-    php${PHP_VERSION}-pgsql \
+    php${PHP_VERSION}-mysql \
     php${PHP_VERSION}-xml \
     php${PHP_VERSION}-zip \
```

</details>

### How to use with Webpack Encore

<details>

<summary>Read the cookbook</summary>

> [!NOTE]
> this cookbook documents the integration of webpack 5+. For older version
> of webpack, use previous version of the docker starter.

If you want to use Webpack Encore in a Symfony project,

1. Follow [instructions on symfony.com](https://symfony.com/doc/current/frontend/encore/installation.html#installing-encore-in-symfony-applications) to install webpack encore.

    You will need to follow [these instructions](https://symfony.com/doc/current/frontend/encore/simple-example.html) too.

2. Create a new service for encore:

    Add the following content to the `docker-compose.yml` file:

    ```yaml
    services:
        encore:
            build:
                context: services/php
                target: builder
            volumes:
                - "../..:/var/www:cached"
            command: >
                yarn run dev-server
                    --hot
                    --host 0.0.0.0
                    --public https://encore.${PROJECT_ROOT_DOMAIN}
                    --allowed-hosts ${PROJECT_ROOT_DOMAIN}
                    --allowed-hosts encore.${PROJECT_ROOT_DOMAIN}
                    --client-web-socket-url-hostname encore.${PROJECT_ROOT_DOMAIN}
                    --client-web-socket-url-port 443
                    --client-web-socket-url-protocol wss
                    --server-type http
            labels:
                - "project-name=${PROJECT_NAME}"
                - "traefik.enable=true"
                - "traefik.http.routers.${PROJECT_NAME}-encore.rule=Host(`encore.${PROJECT_ROOT_DOMAIN}`)"
                - "traefik.http.routers.${PROJECT_NAME}-encore.tls=true"
                - "traefik.http.services.encore.loadbalancer.server.port=8000"
            healthcheck:
            test: ["CMD", "curl", "-f", "http://localhost:8000/build/app.css"]
            profiles:
                - default
    ```

If the assets are not reachable, you may accept self-signed certificate. To do so, open a new tab
at https://encore.app.test and click on accept.

</details>

### How to use with AssetMapper

<details>

<summary>Read the cookbook</summary>

1. Follow [instructions on symfony.com](https://symfony.com/doc/current/frontend/asset_mapper.html#installation) to install AssetMapper.

1. Remove this block in the
`infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` file:

    ```
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
        access_log off;
        add_header Cache-Control "no-cache";
    }
    ```

1. Remove these lines in the `infrastructure/docker/services/php/Dockerfile` file:

    ```diff
    SHELL ["/bin/bash", "-o", "pipefail", "-c"]

    - ARG NODEJS_VERSION=18.x
    - RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor > /usr/share/keyrings/nodesource.gpg \
    -     && echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODEJS_VERSION} bullseye main" > /etc/apt/sources.list.d/nodejs.list

    # Default toys
    RUN apt-get update \
        && apt-get install -y --no-install-recommends \
            git \
            make \
    -       nodejs \
            sudo \
            unzip \
        && apt-get clean \
    -   && npm install -g yarn@1.22 \
        && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
    ```
</details>

### How to add Elasticsearch and Kibana

<details>

<summary>Read the cookbook</summary>

In order to use Elasticsearch and Kibana, you should add the following content
to the `docker-compose.yml` file:

```yaml
volumes:
    elasticsearch-data: {}

services:
    elasticsearch:
        image: elasticsearch:7.8.0
        volumes:
            - elasticsearch-data:/usr/share/elasticsearch/data
        environment:
            - "discovery.type=single-node"
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-elasticsearch.rule=Host(`elasticsearch.${PROJECT_ROOT_DOMAIN}`)"
            - "traefik.http.routers.${PROJECT_NAME}-elasticsearch.tls=true"
        healthcheck:
            test: "curl --fail http://localhost:9200/_cat/health || exit 1"
            interval: 5s
            timeout: 5s
            retries: 5
        profiles:
            - default

    kibana:
        image: kibana:7.8.0
        depends_on:
            - elasticsearch
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-kibana.rule=Host(`kibana.${PROJECT_ROOT_DOMAIN}`)"
            - "traefik.http.routers.${PROJECT_NAME}-kibana.tls=true"
        profiles:
            - default
```

Then, you will be able to browse:

* `https://kibana.<root_domain>`
* `https://elasticsearch.<root_domain>`

In your application, you can use the following configuration:

* scheme: `http`;
* host: `elasticsearch`;
* port: `9200`.

</details>

### How to use with Sylius

<details>

<summary>Read the cookbook</summary>

Add the php extension `gd` to `infrastructure/docker/services/php/Dockerfile`

```
php${PHP_VERSION}-gd \
```

If you want to create a new Sylius project, you need to enter a builder (`inv
builder`) and run the following commands

1. Remove the `application` folder:

    ```bash
    cd ..
    rm -rf application/*
    ```

1. Create a new project:

    ```bash
    composer create-project sylius/sylius-standard application
    ```

1. Configure the `.env`

    ```bash
    sed -i 's#DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\?serverVersion=12\&charset=utf8#' application/.env
    ```

</details>

### How to add RabbitMQ and its dashboard

<details>

<summary>Read the cookbook</summary>

In order to use RabbitMQ and its dashboard, you should add a new service:

```Dockerfile
# services/rabbitmq/Dockerfile
FROM rabbitmq:3-management-alpine

COPY etc/. /etc/
```

And you can add specific RabbitMQ configuration in the `services/rabbitmq/etc/rabbitmq/rabbitmq.conf` file:
```
# services/rabbitmq/etc/rabbitmq/rabbitmq.conf
vm_memory_high_watermark.absolute = 1GB
```

Finally, add the following content to the `docker-compose.yml` file:
```yaml
volumes:
    rabbitmq-data: {}

services:
    rabbitmq:
        build: services/rabbitmq
        volumes:
            - rabbitmq-data:/var/lib/rabbitmq
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-rabbitmq.rule=Host(`rabbitmq.${PROJECT_ROOT_DOMAIN}`)"
            - "traefik.http.routers.${PROJECT_NAME}-rabbitmq.tls=true"
            - "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
        healthcheck:
            test: "rabbitmqctl eval '{ true, rabbit_app_booted_and_running } = { rabbit:is_booted(node()), rabbit_app_booted_and_running }, { [], no_alarms } = { rabbit:alarms(), no_alarms }, [] /= rabbit_networking:active_listeners(), rabbitmq_node_is_healthy.' || exit 1"
            interval: 5s
            timeout: 5s
            retries: 5
        profiles:
            - default
```

In order to publish and consume messages with PHP, you need to install the
`php${PHP_VERSION}-amqp` in the `php` image.

Then, you will be able to browse:

* `https://rabbitmq.<root_domain>` (username: `guest`, password: `guest`)

In your application, you can use the following configuration:

* host: `rabbitmq`;
* username: `guest`;
* password: `guest`;
* port: `rabbitmq`.

For example in Symfony you can use: `MESSENGER_TRANSPORT_DSN=amqp://guest:guest@rabbitmq:5672/%2f/messages`.

</details>

### How to add Redis and its dashboard

<details>

<summary>Read the cookbook</summary>

In order to use Redis and its dashboard, you should add the following content to
the `docker-compose.yml` file:

```yaml
volumes:
    redis-data: {}
    redis-insight-data: {}

services:
    redis:
        image: redis:5
        healthcheck:
            test: ["CMD", "redis-cli", "ping"]
            interval: 5s
            timeout: 5s
            retries: 5
        volumes:
            - "redis-data:/data"
        profiles:
            - default

    redis-insight:
        image: redislabs/redisinsight
        volumes:
            - "redis-insight-data:/db"
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-redis.rule=Host(`redis.${PROJECT_ROOT_DOMAIN}`)"
            - "traefik.http.routers.${PROJECT_NAME}-redis.tls=true"
        profiles:
            - default

```

In order to communicate with Redis, you need to install the
`php${PHP_VERSION}-redis` in the `php` image.

Then, you will be able to browse:

* `https://redis.<root_domain>`

In your application, you can use the following configuration:

* host: `redis`;
* port: `6379`.

</details>

### How to add Mailpit

<details>

<summary>Read the cookbook</summary>

In order to use Mailpit and its dashboard, you should add the following content
to the `docker-compose.yml` file:

```yaml
services:
    mail:
        image: axllent/mailpit
        environment:
            - MP_SMTP_BIND_ADDR=0.0.0.0:25
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-mail.rule=Host(`mail.${PROJECT_ROOT_DOMAIN}`)"
            - "traefik.http.routers.${PROJECT_NAME}-mail.tls=true"
            - "traefik.http.services.mail.loadbalancer.server.port=8025"
        profiles:
            - default
```

Then, you will be able to browse:

* `https://mail.<root_domain>`

In your application, you can use the following configuration:

* scheme: `smtp`;
* host: `mail`;
* port: `25`.

For example in Symfony you can use: `MAILER_DSN=smtp://mail:25`.

</details>

### How to add Mercure

<details>

<summary>Read the cookbook</summary>

In order to use Mercure, you should add the following content to the
`docker-compose.yml` file:

```yaml
services:
    mercure:
        image: dunglas/mercure
        environment:
            - "MERCURE_PUBLISHER_JWT_KEY=password"
            - "MERCURE_SUBSCRIBER_JWT_KEY=password"
            - "ALLOW_ANONYMOUS=1"
            - "CORS_ALLOWED_ORIGINS=*"
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-mercure.rule=Host(`mercure.${PROJECT_ROOT_DOMAIN}`)"
            - "traefik.http.routers.${PROJECT_NAME}-mercure.tls=true"
        profiles:
            - default
```

If you are using Symfony, you must put the following configuration in the `.env` file:

```
MERCURE_PUBLISH_URL=http://mercure/.well-known/mercure
MERCURE_JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6W10sInB1Ymxpc2giOltdfX0.t9ZVMwTzmyjVs0u9s6MI7-oiXP-ywdihbAfPlghTBeQ
```

</details>

### How to add redirection.io

<details>

<summary>Read the cookbook</summary>

In order to use redirection.io, you should add the following content to the
`docker-compose.yml` file to run the agent:

```yaml
services:
    redirectionio-agent:
        build: services/redirectionio-agent
```

Add the following file `infrastructure/docker/services/redirectionio-agent/Dockerfile`:

```Dockerfile
FROM alpine:3.12 AS alpine

WORKDIR /tmp

RUN apk add --no-cache wget ca-certificates \
    && wget https://packages.redirection.io/dist/stable/2/any/redirectionio-agent-latest_any_amd64.tar.gz \
    && tar -xzvf redirectionio-agent-latest_any_amd64.tar.gz

FROM scratch

# Binary copied from tar
COPY --from=alpine /tmp/redirection-agent/redirectionio-agent /usr/local/bin/redirectionio-agent

# Configuration, can be replaced by your own
COPY etc /etc

# Root SSL Certificates, needed as we do HTTPS requests to our service
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

CMD ["/usr/local/bin/redirectionio-agent"]
```

Add `infrastructure/docker/services/redirectionio-agent/etc/redirectionio/agent.yml`:

```yaml
instance_name: "my-instance-dev" ### You may want to change this
listen: 0.0.0.0:10301
```

Then you'll need `wget`. In
`infrastructure/docker/services/php/Dockerfile`, in stage `frontend`:

```Dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        wget \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
```

You can group this command with another one.

Then, **after** installing nginx, you need to install the module:

```Dockerfile
RUN wget -q -O - https://packages.redirection.io/gpg.key | gpg --dearmor > /usr/share/keyrings/redirection.io.gpg \
    && echo "deb [signed-by=/usr/share/keyrings/redirection.io.gpg] https://packages.redirection.io/deb/stable/2 focal main" | tee -a /etc/apt/sources.list.d/packages_redirection_io_deb.list \
    && apt-get update \
    && apt-get install libnginx-mod-redirectionio \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
```

Finally, you need to edit
`infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` to add the
following configuration in the `server` block:

```
redirectionio_pass redirectionio-agent:10301;
redirectionio_project_key "AAAAAAAAAAAAAAAA:BBBBBBBBBBBBBBBB";
```

**Don't forget to change the project key**.

</details>

### How to add Blackfire.io

<details>

<summary>Read the cookbook</summary>

In order to use Blackfire.io, you should add the following content to the
`docker-compose.yml` file to run the agent:

```yaml
services:
    blackfire:
        image: blackfire/blackfire
        environment:
            BLACKFIRE_SERVER_ID: FIXME
            BLACKFIRE_SERVER_TOKEN: FIXME
            BLACKFIRE_CLIENT_ID: FIXME
            BLACKFIRE_CLIENT_TOKEN: FIXME
        profiles:
            - default

```

Then you'll need `wget`. In
`infrastructure/docker/services/php/Dockerfile`, in stage `base`:

```Dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        wget \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
```

You can group this command with another one.

Then, **after** installing PHP, you need to install the probe:

```Dockerfile
RUN wget -q -O - https://packages.blackfire.io/gpg.key | gpg --dearmor > /usr/share/keyrings/blackfire.io.gpg \
    && sh -c 'echo "deb [signed-by=/usr/share/keyrings/blackfire.io.gpg] http://packages.blackfire.io/debian any main" > /etc/apt/sources.list.d/blackfire.list' \
    && apt-get update \
    && apt-get install -y --no-install-recommends \
        blackfire-php \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \
    && sed -i 's#blackfire.agent_socket.*#blackfire.agent_socket=tcp://blackfire:8707#' /etc/php/${PHP_VERSION}/mods-available/blackfire.ini
```

If you want to profile HTTP calls, you need to enable the probe with PHP-FPM.
So in `infrastructure/docker/services/php/Dockerfile`:

```Dockerfile
RUN phpenmod blackfire
```

Here also, You can group this command with another one.

</details>

### How to add support for crons?

<details>

<summary>Read the cookbook</summary>

In order to set up crontab, you should add a new container:

```Dockerfile
# services/php/Dockerfile

FROM php-base AS cron

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        cron \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*

COPY --link cron/crontab /etc/cron.d/crontab
COPY --link cron/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

CMD ["cron", "-f"]
```

And you can add all your crons in the `services/php/crontab` file:
```crontab
* * * * * php -r 'echo time().PHP_EOL;' > /tmp/cron-stdout 2>&1
```

And you can add the following content to the `services/php/entrypoint.sh` file:
```bash
#!/bin/bash
set -e

groupadd -g $USER_ID app
useradd -M -u $USER_ID -g $USER_ID -s /bin/bash app

crontab -u app /etc/cron.d/crontab

# Wrapper for logs
FIFO=/tmp/cron-stdout
rm -f $FIFO
mkfifo $FIFO
chmod 0666 $FIFO
while true; do
  cat /tmp/cron-stdout
done &

exec "$@"
```

Finally, add the following content to the `docker-compose.yml` file:
```yaml
services:
    cron:
        build:
            context: services/php
            target: cron
            cache_from:
                - "type=registry,ref=${REGISTRY:-}/cron:cache"
        # depends_on:
        #     postgres:
        #         condition: service_healthy
        env_file: .env
        environment:
            USER_ID: ${USER_ID}
        volumes:
            - "../..:/var/www:cached"
            - "../../.home:/home/app:cached"
        profiles:
            - default
```

</details>

### How to run workers?

<details>

<summary>Read the cookbook</summary>

In order to set up workers, you should define their services in the `docker-compose.worker.yml` file:

```yaml
services:
    worker_my_worker:
        <<: *worker_base
        command: /var/www/application/my-worker

    worker_date:
        <<: *worker_base
        command: watch -n 1 date
```

</details>

### How to use PHP FPM status page?

<details>

<summary>Read the cookbook</summary>

If you want to use the [PHP FPM status
page](https://www.php.net/manual/en/fpm.status.php) you need to remove a
configuration block in the
`infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf` file:

```diff
-        # Remove this block if you want to access to PHP FPM monitoring
-        # dashboarsh (on URL: /php-fpm-status). WARNING: on production, you must
-        # secure this page (by user IP address, with a password, for example)
-        location ~ ^/php-fpm-status$ {
-            deny all;
-        }
-
```

And if your application uses the front controller pattern, and you want to see
the real request URI, you also need to uncomment the following configuration
block:

```diff
-            # # Uncomment if you want to use /php-fpm-status endpoint **with**
-            # # real request URI. It may have some side effects, that's why it's
-            # # commented by default
-            # fastcgi_param SCRIPT_NAME $request_uri;
+            # Uncomment if you want to use /php-fpm-status endpoint **with**
+            # real request URI. It may have some side effects, that's why it's
+            # commented by default
+            fastcgi_param SCRIPT_NAME $request_uri;
```

</details>

### How to pg_activity for monitoring PostgreSQL

<details>

<summary>Read the cookbook</summary>

In order to install pg_activity, you should add the following content to the
`infrastructure/docker/services/postgres/Dockerfile` file:

```Dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        pg-activity \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
```

Then, you can add the following content to the `castor.php` file:

```php
#[AsTask(description: 'Monitor PostgreSQL', namespace: 'app:db')]
function pg_activity(): void
{
    docker_compose('exec postgres pg_activity -U app');
}
```

Finally you can use the following command:

```
castor app:db:pg-activity
```

</details>

### Docker For Windows support

<details>

<summary>Read the cookbook</summary>

This starter kit is compatible with Docker for Windows, so you can enjoy native Docker experience on Windows. You will have to keep in mind some differences:

- You will be prompted to run the env vars manually if you use PowerShell.
</details>

### How to access a container via hostname from another container

<details>

<summary>Read the cookbook</summary>

Let's say you have a container (`frontend`) that responds to many hostnames:
`app.test`, `api.app.test`, `admin.app.test`. And you have another container
(`builder`) that needs to call the `frontend` with a specific hostname - or with
HTTPS. This is usually the case when you have a functional test suite.

To enable this feature, you need to add `extra_hosts` to the `builder` container
like so:

```yaml
services:
    builder:
        # [...]
        extra_hosts:
            - "app.test:host-gateway"
            - "api.app.test:host-gateway"
            - "admin.app.test:host-gateway"
```

</details>

### How to connect networks of two projects

<details>

<summary>Read the cookbook</summary>

Let's say you have two projects `foo` and `bar`. You want to run both projects a
the same time. And containers from `foo` project should be able to dialog with
`bar` project via public network (host network).

In the `foo` project, you'll need to declare the `bar_default` network in
`docker-compose.yml`:

```yaml
networks:
    bar_default:
        external: true
```

Then, attach it to the the `foo` router:

```yaml
services:
    router:
        networks:
            - default
            - bar_default
```

Finally, you must remove the constraints on the router so it'll be able to
discover containers from another docker compose project:

```diff
--- a/infrastructure/docker/services/router/traefik/traefik.yaml
+++ b/infrastructure/docker/services/router/traefik/traefik.yaml
 providers:
   docker:
     exposedByDefault: false
-    constraints: "Label(`project-name`,`{{ PROJECT_NAME }}`)"
   file:
```

Finally, you must :

1. build the project `foo`
1. build the project `bar`
1.  Create the network `bar_default` (first time only)
    ```
    docker network create bar_default
    ```
1. start the project `foo`
1. start the project `bar`

</details>

### How to use FrankenPHP

<details>

<summary>Read the cookbook</summary>

Migrating to FrankenPHP involves a lot of changes. You can take inspiration from
the following [repostory](https://github.com/lyrixx/async-messenger-mercure)
and specifically [this commit](https://github.com/lyrixx/async-messenger-mercure/commit/9ac8776253f3950a6c57d457b3742923f9e096a7).

</details>

### How to use a docker registry to cache images layer

<details>

<summary>Read the cookbook</summary>

You can use a docker registry to cache images layer, it can be useful to speed
up the build process during the CI and local development.

First you need a docker registry, in following examples we will use the GitHub
registry (ghcr.io).

Then add the registry to the context variable of the `castor.php` file:

```php
function create_default_variables(): Context
{
    return [
        // [...]
        'registry' => 'ghcr.io/your-organization/your-project',
    ];
}
```

Once you have the registry, you can push the images to the registry:

```bash
castor docker:push
```

> Pushing image cache from a dev environment to a registry is not recommended,
> as cache may be sensitive to the environment and may not be compatible with
> other environments. It happens, for example, when you add some build args
> depending on your environment. It is recommended to push the cache from the CI
> environment.

This command will generate a bake file with the images to push from the
`cache_from` directive of the `docker-compose.yml` file. If you want to add more
images to push, you can add the `cache_from` directive to them.

```yaml
services:
    my-service:
        build:
            cache_from:
                - "type=registry,ref=${REGISTRY:-}/my-service:cache"
```

#### How to use cached images in a GitHub action

##### Pushing images to the registry from a GitHub action

1. Ensure that the github token have the `write:packages` scope:

```yaml
permissions:
    contents: read
    packages: write
```

2. Install Docker buildx in the github action:

```yaml
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
```

3. Login to the registry:

```yaml
    - name: Log in to registry
      shell: bash
      run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
```

##### Using the cached images in GitHub action

By default images are private in the GitHub registry, you will need to login to
the registry to pull the images:

```yaml
    - name: Log in to registry
      shell: bash
      run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
```

</details>

## Credits

Docker-starter logo was created by [Caneco](https://twitter.com/caneco).

<br><br>
<div align="center">
<a href="https://jolicode.com/"><img src="https://jolicode.com/media/original/oss/footer-github.png?v3" alt="JoliCode is sponsoring this project"></a>
</div>


================================================
FILE: application/public/index.php
================================================
<?php

echo 'Hello world from PHP ', \PHP_MAJOR_VERSION, '.', \PHP_MINOR_VERSION, '.', \PHP_RELEASE_VERSION, " inside Docker! \n";

echo 'Environment: ', $_SERVER['APP_ENV'] ?? 'not set', "\n";


================================================
FILE: castor.php
================================================
<?php

use Castor\Attribute\AsTask;

use function Castor\guard_min_version;
use function Castor\import;
use function Castor\io;
use function Castor\notify;
use function Castor\variable;
use function docker\about;
use function docker\build;
use function docker\docker_compose_run;
use function docker\up;

// use function docker\workers_start;
// use function docker\workers_stop;

guard_min_version('0.26.0');

import(__DIR__ . '/.castor');

/**
 * @return array{project_name: string, root_domain: string, extra_domains: string[], php_version: string}
 */
function create_default_variables(): array
{
    $projectName = 'app';
    $tld = 'test';

    return [
        'project_name' => $projectName,
        'root_domain' => "{$projectName}.{$tld}",
        'extra_domains' => [
            "www.{$projectName}.{$tld}",
        ],
        // In order to test docker stater, we need a way to pass different values.
        // You should remove the `$_SERVER` and hardcode your configuration.
        'php_version' => $_SERVER['DS_PHP_VERSION'] ?? '8.5',
        'registry' => $_SERVER['DS_REGISTRY'] ?? null,
    ];
}

#[AsTask(description: 'Builds and starts the infrastructure, then install the application (composer, yarn, ...)')]
function start(): void
{
    io()->title('Starting the stack');

    // workers_stop();
    build();
    install();
    up(profiles: ['default']); // We can't start worker now, they are not installed
    migrate();
    // workers_start();

    notify('The stack is now up and running.');
    io()->success('The stack is now up and running.');

    about();
}

#[AsTask(description: 'Installs the application (composer, yarn, ...)', namespace: 'app', aliases: ['install'])]
function install(): void
{
    io()->title('Installing the application');

    $basePath = sprintf('%s/application', variable('root_dir'));

    if (is_file("{$basePath}/composer.json")) {
        io()->section('Installing PHP dependencies');
        docker_compose_run('composer install -n --prefer-dist --optimize-autoloader');
    }
    if (is_file("{$basePath}/yarn.lock")) {
        io()->section('Installing Node.js dependencies');
        docker_compose_run('yarn install --frozen-lockfile');
    } elseif (is_file("{$basePath}/package.json")) {
        io()->section('Installing Node.js dependencies');

        if (is_file("{$basePath}/package-lock.json")) {
            docker_compose_run('npm ci');
        } else {
            docker_compose_run('npm install');
        }
    }
    if (is_file("{$basePath}/importmap.php")) {
        io()->section('Installing importmap');
        docker_compose_run('bin/console importmap:install');
    }

    qa\install();
}

#[AsTask(description: 'Update dependencies')]
function update(bool $withTools = false): void
{
    io()->title('Updating dependencies...');

    // docker_compose_run('composer update -o');

    if ($withTools) {
        qa\update();
    }
}

#[AsTask(description: 'Clears the application cache', namespace: 'app', aliases: ['cache-clear'])]
function cache_clear(bool $warm = true): void
{
    // io()->title('Clearing the application cache');

    // docker_compose_run('rm -rf var/cache/');

    // if ($warm) {
    //     cache_warmup();
    // }
}

#[AsTask(description: 'Warms the application cache', namespace: 'app', aliases: ['cache-warmup'])]
function cache_warmup(): void
{
    // io()->title('Warming the application cache');

    // docker_compose_run('bin/console cache:warmup', c: context()->withAllowFailure());
}

#[AsTask(description: 'Migrates database schema', namespace: 'app:db', aliases: ['migrate'])]
function migrate(): void
{
    // io()->title('Migrating the database schema');

    // docker_compose_run('bin/console doctrine:database:create --if-not-exists');
    // docker_compose_run('bin/console doctrine:migration:migrate -n --allow-no-migration --all-or-nothing');
}

#[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['fixtures'])]
function fixtures(): void
{
    // io()->title('Loads fixtures');

    // docker_compose_run('bin/console doctrine:fixture:load -n');
}


================================================
FILE: infrastructure/docker/docker-compose.dev.yml
================================================
services:
    router:
        build: services/router
        volumes:
            - "/var/run/docker.sock:/var/run/docker.sock"
            - "./services/router/certs:/etc/ssl/certs"
        ports:
            - "80:80"
            - "443:443"
            - "8080:8080"
        networks:
            - default
        profiles:
            - default


================================================
FILE: infrastructure/docker/docker-compose.yml
================================================
# Templates to factorize the service definitions
x-templates:
    worker_base: &worker_base
        build:
            context: services/php
            target: worker
        user: "${USER_ID}:${USER_ID}"
        environment:
            - APP_ENV
        depends_on:
            postgres:
                condition: service_healthy
        volumes:
            - "../..:/var/www:cached"
        profiles:
            - worker

volumes:
    postgres-data: {}
    # # Needed if $XDG_ env vars have been overridden
    # builder-yarn-data: {}

services:
    postgres:
        image: postgres:16
        environment:
            - POSTGRES_USER=app
            - POSTGRES_PASSWORD=app
        volumes:
            - postgres-data:/var/lib/postgresql/data
        healthcheck:
            test: ["CMD-SHELL", "pg_isready -U postgres"]
            interval: 5s
            timeout: 5s
            retries: 5
        profiles:
            - default

    frontend:
        build:
            context: services/php
            target: frontend
            cache_from:
              - "type=registry,ref=${REGISTRY:-}/frontend:cache"
        user: "${USER_ID}:${USER_ID}"
        environment:
            - APP_ENV
        volumes:
            - "../..:/var/www:cached"
            - "../../.home:/home/app:cached"
        depends_on:
            postgres:
                condition: service_healthy
        profiles:
            - default
        labels:
            - "traefik.enable=true"
            - "project-name=${PROJECT_NAME}"
            - "traefik.http.routers.${PROJECT_NAME}-frontend.rule=Host(${PROJECT_DOMAINS})"
            - "traefik.http.routers.${PROJECT_NAME}-frontend.tls=true"
            - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.rule=Host(${PROJECT_DOMAINS})"
            # Comment the next line to be able to access frontend via HTTP instead of HTTPS
            - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file"

    # worker_messenger:
    #     <<: *worker_base
    #     command: php -d memory_limit=1G /var/www/application/bin/console messenger:consume async --memory-limit=128M

    builder:
        build:
            context: services/php
            target: builder
            cache_from:
                - "type=registry,ref=${REGISTRY:-}/builder:cache"
        init: true
        user: "${USER_ID}:${USER_ID}"
        environment:
            - APP_ENV
            # The following list contains the common environment variables exposed by CI platforms
            - GITHUB_ACTIONS
            - CI # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari
            - CONTINUOUS_INTEGRATION # Travis CI, Cirrus CI
            - BUILD_NUMBER # Jenkins, TeamCity
            - RUN_ID # TaskCluster, dsari
        volumes:
            - "../..:/var/www:cached"
            - "../../.home:/home/app:cached"
            # Needed when $XDG_ env vars have overridden, to persist the yarn
            # cache between builder and watcher, adapt according to the location
            # of $XDG_DATA_HOME
            # - "builder-yarn-data:/data/yarn"
        depends_on:
            - postgres
        profiles:
            - builder


================================================
FILE: infrastructure/docker/services/php/Dockerfile
================================================
# hadolint global ignore=DL3008

FROM debian:12.8-slim AS php-base

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        curl \
        ca-certificates \
        gnupg \
    && curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb \
    && dpkg -i /tmp/debsuryorg-archive-keyring.deb \
    && echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ bookworm main" > /etc/apt/sources.list.d/sury.list \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        bash-completion \
        procps \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*

ARG PHP_VERSION

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        "php${PHP_VERSION}-apcu" \
        "php${PHP_VERSION}-bcmath" \
        "php${PHP_VERSION}-cli" \
        "php${PHP_VERSION}-common" \
        "php${PHP_VERSION}-curl" \
        "php${PHP_VERSION}-iconv" \
        "php${PHP_VERSION}-intl" \
        "php${PHP_VERSION}-mbstring" \
        "php${PHP_VERSION}-pgsql" \
        "php${PHP_VERSION}-uuid" \
        "php${PHP_VERSION}-xml" \
        "php${PHP_VERSION}-zip" \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*

# Configuration
COPY base/php-configuration /etc/php/${PHP_VERSION}

ENV PHP_VERSION=${PHP_VERSION}
ENV HOME=/home/app
ENV COMPOSER_MEMORY_LIMIT=-1

WORKDIR /var/www

FROM php-base AS frontend

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        nginx \
        "php${PHP_VERSION}-fpm" \
        runit \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \
    && rm -r "/etc/php/${PHP_VERSION}/fpm/pool.d/"

RUN useradd -s /bin/false nginx

COPY frontend/php-configuration /etc/php/${PHP_VERSION}
COPY frontend/etc/nginx/. /etc/nginx/
RUN rm -rf /etc/service/
COPY frontend/etc/service/. /etc/service/
RUN chmod 777 /etc/service/*/supervise/

RUN phpenmod app-default \
    && phpenmod app-fpm

EXPOSE 80

CMD ["runsvdir", "-P", "/etc/service"]

FROM php-base AS worker

FROM php-base AS builder

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

ARG NODEJS_VERSION=24.x
RUN curl -s https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \
    && echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODEJS_VERSION} nodistro main" > /etc/apt/sources.list.d/nodesource.list

# Default toys
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        "git" \
        "make" \
        "nodejs" \
        "php${PHP_VERSION}-dev" \
        "sudo" \
        "unzip" \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \
    && corepack enable \
    && yarn set version stable

# Install a fake sudo command
# This is commented out by default because it exposes a security risk if you use this image in production, but it may be useful for development
# Use it at your own risk
# COPY base/sudo.sh /usr/local/bin/sudo
# RUN curl -L https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64 -o /usr/local/bin/gosu && \
#    chmod u+s /usr/local/bin/gosu && \
#    chmod +x /usr/local/bin/gosu && \
#    chmod +x /usr/local/bin/sudo

# Config
COPY builder/php-configuration /etc/php/${PHP_VERSION}
RUN phpenmod app-default \
    && phpenmod app-builder

# Composer
COPY --from=composer/composer:2.9.5 /usr/bin/composer /usr/bin/composer

# Pie
RUN curl -L --output /usr/local/bin/pie https://github.com/php/pie/releases/download/1.3.9/pie.phar \
    && chmod +x /usr/local/bin/pie

# Autocompletion
ADD https://raw.githubusercontent.com/symfony/symfony/refs/heads/7.3/src/Symfony/Component/Console/Resources/completion.bash /tmp/completion.bash

# Composer symfony/console version is too old, and doest not support "API version feature", so we remove it
# Hey, while we are at it, let's add some more completion
RUN sed /tmp/completion.bash \
        -e "s/{{ COMMAND_NAME }}/composer/g" \
        -e 's/"-a{{ VERSION }}"//g' \
        -e "s/{{ VERSION }}/1/g"  \
        > /etc/bash_completion.d/composer \
    && sed /tmp/completion.bash \
        -e "s/{{ COMMAND_NAME }}/console/g" \
        -e "s/{{ VERSION }}/1/g"  \
        > /etc/bash_completion.d/console

# Third party tools
ENV PATH="$PATH:/var/www/tools/bin"

# Good default customization
RUN cat >> /etc/bash.bashrc <<EOF
. /etc/bash_completion

PS1='\[\e[01;33m\]\u \[\e[00;32m\]\w\[\e[0m\] '
EOF

WORKDIR /var/www/application


================================================
FILE: infrastructure/docker/services/php/base/php-configuration/mods-available/app-default.ini
================================================
; priority=30
[PHP]
short_open_tag = Off
memory_limit = 512M
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
error_log = /proc/self/fd/2
log_errors = On
log_errors_max_len = 0
max_execution_time = 0
always_populate_raw_post_data = -1
upload_max_filesize = 20M
post_max_size = 20M
[Date]
date.timezone = UTC
[Phar]
phar.readonly = Off
[opcache]
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
realpath_cache_size=4096K
realpath_cache_ttl=600
opcache.error_log = /proc/self/fd/2
[apc]
apc.enabled=1
apc.enable_cli=1


================================================
FILE: infrastructure/docker/services/php/base/sudo.sh
================================================
#!/usr/bin/env bash

export 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."
exec gosu 0:0 $@


================================================
FILE: infrastructure/docker/services/php/builder/php-configuration/mods-available/app-builder.ini
================================================
; priority=40
[PHP]
error_log = /var/log/php/error.log
[opcache]
opcache.error_log = /var/log/php/opcache.log


================================================
FILE: infrastructure/docker/services/php/frontend/etc/nginx/environments
================================================


================================================
FILE: infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf
================================================
user      nginx;
pid       /tmp/nginx.pid;
daemon    off;
error_log /proc/self/fd/2;
include /etc/nginx/modules-enabled/*.conf;

http {
    access_log /proc/self/fd/1;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    client_max_body_size 20m;
    server_tokens off;

    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;

    client_body_temp_path /tmp/nginx-client_body_temp_path;
    fastcgi_temp_path /tmp/nginx-fastcgi_temp_path;
    proxy_temp_path /tmp/nginx-proxy_temp_path;
    scgi_temp_path /tmp/nginx-scgi_temp_path;
    uwsgi_temp_path /tmp/nginx-uwsgi_temp_path;

    server {
        listen 0.0.0.0:80;
        root /var/www/application/public;

        location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
            access_log off;
            add_header Cache-Control "no-cache";
        }

        # Remove this block if you want to access to PHP FPM monitoring
        # dashboard (on URL: /php-fpm-status). WARNING: on production, you must
        # secure this page (by user IP address, with a password, for example)
        location ~ ^/php-fpm-status$ {
            deny all;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_pass 127.0.0.1:9000;
        }

        location / {
            # try to serve file directly, fallback to index.php
            try_files $uri /index.php$is_args$args;
        }

        location ~ ^/index\.php(/|$) {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_split_path_info ^(.+\.php)(/.*)$;

            include fastcgi_params;
            include environments;

            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param HTTPS on;
            fastcgi_param SERVER_NAME $http_host;
            # # Uncomment if you want to use /php-fpm-status endpoint **with**
            # # real request URI. It may have some side effects, that's why it's
            # # commented by default
            # fastcgi_param SCRIPT_NAME $request_uri;
        }

        error_log  /proc/self/fd/2;
        access_log /proc/self/fd/1;
    }
}

events {}


================================================
FILE: infrastructure/docker/services/php/frontend/etc/service/nginx/run
================================================
#!/bin/sh

exec /usr/sbin/nginx


================================================
FILE: infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore
================================================
/*
!.gitignore


================================================
FILE: infrastructure/docker/services/php/frontend/etc/service/php-fpm/run
================================================
#!/bin/sh

exec /usr/sbin/php-fpm${PHP_VERSION} -y /etc/php/${PHP_VERSION}/fpm/php-fpm.conf -O


================================================
FILE: infrastructure/docker/services/php/frontend/etc/service/php-fpm/supervise/.gitignore
================================================
/*
!.gitignore


================================================
FILE: infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf
================================================
[global]
error_log  = /proc/self/fd/2
daemonize  = no

[www]
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 25
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 3
pm.max_requests = 500
pm.status_path = /php-fpm-status
clear_env  = no
request_terminate_timeout = 120s
catch_workers_output = yes


================================================
FILE: infrastructure/docker/services/php/frontend/php-configuration/mods-available/app-fpm.ini
================================================
; priority=40
[PHP]
expose_php = off
memory_limit = 128M
max_execution_time = 30


================================================
FILE: infrastructure/docker/services/router/Dockerfile
================================================
FROM traefik:v3.6.1

COPY traefik /etc/traefik

ARG PROJECT_NAME
RUN sed -i "s/{{ PROJECT_NAME }}/${PROJECT_NAME}/g" /etc/traefik/traefik.yaml

VOLUME [ "/etc/ssl/certs" ]


================================================
FILE: infrastructure/docker/services/router/certs/.gitkeep
================================================


================================================
FILE: infrastructure/docker/services/router/generate-ssl.sh
================================================
#!/usr/bin/env bash

# Script used in dev to generate a basic SSL cert

BASE=$(dirname $0)

CERTS_DIR=$BASE/certs

rm -rf $CERTS_DIR
mkdir -p $CERTS_DIR
touch $CERTS_DIR/.gitkeep

openssl req -x509 -sha256 -newkey rsa:4096 \
    -keyout $CERTS_DIR/key.pem \
    -out $CERTS_DIR/cert.pem \
    -days 3650 -nodes -config \
    $BASE/openssl.cnf


================================================
FILE: infrastructure/docker/services/router/openssl.cnf
================================================
# Configuration used in dev to generate a basic SSL cert
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions	= v3_req

[v3_req]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints=CA:true
subjectAltName = @alt_names

[dn]
CN=app.test

[alt_names]
DNS.1 = app.test


================================================
FILE: infrastructure/docker/services/router/traefik/dynamic_conf.yaml
================================================
tls:
  stores:
    default:
      defaultCertificate:
        certFile: /etc/ssl/certs/cert.pem
        keyFile: /etc/ssl/certs/key.pem

http:
  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https


================================================
FILE: infrastructure/docker/services/router/traefik/traefik.yaml
================================================
global:
  checkNewVersion: false
  sendAnonymousUsage: false

providers:
  docker:
    exposedByDefault: false
    constraints: "Label(`project-name`,`{{ PROJECT_NAME }}`)"
  file:
    filename: /etc/traefik/dynamic_conf.yaml

# # Uncomment get all DEBUG logs
#log:
#    level: "DEBUG"

# # Uncomment to view all access logs
#accessLog: {}

api:
  dashboard: true
  insecure: true # No authentication are required

entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"
  traefik: # this one exists by default
    address: ":8080"


================================================
FILE: phpstan.neon
================================================
parameters:
    level: 8
    paths:
        # - application/src
        - application/public
        - castor.php
        - .castor/
    scanFiles:
        - .castor.stub.php
    # scanDirectories:
    #     - application/vendor
    tmpDir: tools/phpstan/var
    inferPrivatePropertyTypeFromConstructor: true

    # symfony:
    #     containerXmlPath: 'application/var/cache/dev/App_KernelDevDebugContainer.xml'

    typeAliases:
        ContextData: '''
            array{
                project_name: string,
                root_domain: string,
                extra_domains: string[],
                php_version: string,
                docker_compose_files: list<string>,
                docker_compose_run_environment: list<string>,
                macos: bool,
                power_shell: bool,
                user_id: int,
                root_dir: string,
                registry?: ?string,
            }
        '''


================================================
FILE: tools/php-cs-fixer/.gitignore
================================================
/vendor/


================================================
FILE: tools/php-cs-fixer/composer.json
================================================
{
    "type": "project",
    "require": {
        "friendsofphp/php-cs-fixer": "^3.76.0"
    },
    "config": {
        "platform": {
            "php": "8.3"
        },
        "bump-after-update": true,
        "sort-packages": true
    }
}


================================================
FILE: tools/phpstan/.gitignore
================================================
/var/
/vendor/


================================================
FILE: tools/phpstan/composer.json
================================================
{
    "type": "project",
    "require": {
        "phpstan/extension-installer": "^1.4.3",
        "phpstan/phpstan": "^2.1.17",
        "phpstan/phpstan-deprecation-rules": "^2.0.3",
        "phpstan/phpstan-symfony": "^2.0.6"
    },
    "config": {
        "allow-plugins": {
            "phpstan/extension-installer": true
        },
        "bump-after-update": true,
        "platform": {
            "php": "8.3"
        },
        "sort-packages": true
    }
}


================================================
FILE: tools/twig-cs-fixer/.gitignore
================================================
/vendor/


================================================
FILE: tools/twig-cs-fixer/composer.json
================================================
{
    "type": "project",
    "require": {
        "vincentlanglet/twig-cs-fixer": "^3.8.1"
    },
    "config": {
        "platform": {
            "php": "8.3"
        },
        "bump-after-update": true,
        "sort-packages": true
    }
}
Download .txt
gitextract_is9p0cpd/

├── .castor/
│   ├── context.php
│   ├── database.php
│   ├── docker.php
│   ├── init.php
│   └── qa.php
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── cache.yml
│       └── ci.yml
├── .gitignore
├── .home/
│   └── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.dist.md
├── README.md
├── application/
│   └── public/
│       └── index.php
├── castor.php
├── infrastructure/
│   └── docker/
│       ├── docker-compose.dev.yml
│       ├── docker-compose.yml
│       └── services/
│           ├── php/
│           │   ├── Dockerfile
│           │   ├── base/
│           │   │   ├── php-configuration/
│           │   │   │   └── mods-available/
│           │   │   │       └── app-default.ini
│           │   │   └── sudo.sh
│           │   ├── builder/
│           │   │   └── php-configuration/
│           │   │       └── mods-available/
│           │   │           └── app-builder.ini
│           │   └── frontend/
│           │       ├── etc/
│           │       │   ├── nginx/
│           │       │   │   ├── environments
│           │       │   │   └── nginx.conf
│           │       │   └── service/
│           │       │       ├── nginx/
│           │       │       │   ├── run
│           │       │       │   └── supervise/
│           │       │       │       └── .gitignore
│           │       │       └── php-fpm/
│           │       │           ├── run
│           │       │           └── supervise/
│           │       │               └── .gitignore
│           │       └── php-configuration/
│           │           ├── fpm/
│           │           │   └── php-fpm.conf
│           │           └── mods-available/
│           │               └── app-fpm.ini
│           └── router/
│               ├── Dockerfile
│               ├── certs/
│               │   └── .gitkeep
│               ├── generate-ssl.sh
│               ├── openssl.cnf
│               └── traefik/
│                   ├── dynamic_conf.yaml
│                   └── traefik.yaml
├── phpstan.neon
└── tools/
    ├── php-cs-fixer/
    │   ├── .gitignore
    │   └── composer.json
    ├── phpstan/
    │   ├── .gitignore
    │   └── composer.json
    └── twig-cs-fixer/
        ├── .gitignore
        └── composer.json
Download .txt
SYMBOL INDEX (39 symbols across 6 files)

FILE: .castor/context.php
  function create_default_context (line 11) | #[AsContext(default: true)]
  function create_test_context (line 61) | #[AsContext(name: 'test')]
  function create_ci_context (line 73) | #[AsContext(name: 'ci')]

FILE: .castor/database.php
  function postgres_client (line 9) | #[AsTask(description: 'Connect to the PostgreSQL database', name: 'db:cl...

FILE: .castor/docker.php
  function about (line 27) | #[AsTask(description: 'Displays some help and available urls for the cur...
  function open_project (line 64) | #[AsTask(description: 'Opens the project in your browser', namespace: ''...
  function build (line 70) | #[AsTask(description: 'Builds the infrastructure', aliases: ['build'])]
  function up (line 106) | #[AsTask(description: 'Builds and starts the infrastructure', aliases: [...
  function stop (line 137) | #[AsTask(description: 'Stops the infrastructure', aliases: ['stop'])]
  function builder (line 160) | #[AsTask(description: 'Opens a shell (bash) or proxy any command to the ...
  function logs (line 178) | #[AsTask(description: 'Displays infrastructure logs', aliases: ['logs'])]
  function ps (line 193) | #[AsTask(description: 'Lists containers status', aliases: ['ps'])]
  function destroy (line 213) | #[AsTask(description: 'Cleans the infrastructure (remove container, volu...
  function generate_certificates (line 239) | #[AsTask(description: 'Generates SSL certificates (with mkcert if availa...
  function workers_start (line 308) | #[AsTask(description: 'Starts the workers', namespace: 'docker:worker', ...
  function workers_stop (line 345) | #[AsTask(description: 'Stops the workers', namespace: 'docker:worker', n...
  function docker_compose (line 375) | function docker_compose(array $subCommand, ?Context $c = null, array $pr...
  function docker_compose_run (line 418) | function docker_compose_run(
  function docker_exit_code (line 459) | function docker_exit_code(
  function run_in_docker_or_locally_for_mac (line 481) | function run_in_docker_or_locally_for_mac(string $command, ?Context $c =...
  function push (line 492) | #[AsTask(description: 'Push images cache to the registry', namespace: 'd...
  function get_services (line 587) | function get_services(): array
  function get_service_names (line 602) | function get_service_names(): array

FILE: .castor/init.php
  function init (line 10) | #[AsTask(description: 'Initialize the project')]
  function symfony (line 34) | #[AsTask(description: 'Install Symfony')]

FILE: .castor/qa.php
  function all (line 14) | #[AsTask(description: 'Runs all QA tasks')]
  function install (line 25) | #[AsTask(description: 'Installs tooling')]
  function update (line 35) | #[AsTask(description: 'Updates tooling')]
  function phpstan (line 56) | #[AsTask(description: 'Runs PHPStan', aliases: ['phpstan'])]
  function cs (line 73) | #[AsTask(description: 'Fixes Coding Style', aliases: ['cs'])]
  function twigCs (line 89) | #[AsTask(description: 'Fixes Twig Coding Style', aliases: ['twig-cs'])]

FILE: castor.php
  function create_default_variables (line 25) | function create_default_variables(): array
  function start (line 43) | #[AsTask(description: 'Builds and starts the infrastructure, then instal...
  function install (line 61) | #[AsTask(description: 'Installs the application (composer, yarn, ...)', ...
  function update (line 92) | #[AsTask(description: 'Update dependencies')]
  function cache_clear (line 104) | #[AsTask(description: 'Clears the application cache', namespace: 'app', ...
  function cache_warmup (line 116) | #[AsTask(description: 'Warms the application cache', namespace: 'app', a...
  function migrate (line 124) | #[AsTask(description: 'Migrates database schema', namespace: 'app:db', a...
  function fixtures (line 133) | #[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['...
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (104K chars).
[
  {
    "path": ".castor/context.php",
    "chars": 2547,
    "preview": "<?php\n\nnamespace docker;\n\nuse Castor\\Attribute\\AsContext;\nuse Castor\\Context;\nuse Symfony\\Component\\Process\\Process;\n\nus"
  },
  {
    "path": ".castor/database.php",
    "chars": 430,
    "preview": "<?php\n\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\context;\nuse function Castor\\io;\nuse function docker\\docker_com"
  },
  {
    "path": ".castor/docker.php",
    "chars": 17669,
    "preview": "<?php\n\nnamespace docker;\n\nuse Castor\\Attribute\\AsOption;\nuse Castor\\Attribute\\AsRawTokens;\nuse Castor\\Attribute\\AsTask;\n"
  },
  {
    "path": ".castor/init.php",
    "chars": 1558,
    "preview": "<?php\n\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\fs;\nuse function Castor\\variable;\nuse function docker\\build;\nus"
  },
  {
    "path": ".castor/qa.php",
    "chars": 2935,
    "preview": "<?php\n\nnamespace qa;\n\n// use Castor\\Attribute\\AsRawTokens;\nuse Castor\\Attribute\\AsOption;\nuse Castor\\Attribute\\AsTask;\n\n"
  },
  {
    "path": ".gitattributes",
    "chars": 66,
    "preview": "# Force LF line ending (mandatory for Windows)\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/workflows/cache.yml",
    "chars": 772,
    "preview": "name: Push docker image to registry\n\n\"on\":\n  push:\n    # Only run this job when pushing to the main branch\n    branches:"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 3707,
    "preview": "name: Continuous Integration\n\n\"on\":\n    push:\n        branches: [\"main\"]\n    pull_request:\n        branches: [\"main\"]\n  "
  },
  {
    "path": ".gitignore",
    "chars": 170,
    "preview": "/.castor.stub.php\n/infrastructure/docker/docker-compose.override.yml\n/infrastructure/docker/services/router/certs/*.pem\n"
  },
  {
    "path": ".home/.gitignore",
    "chars": 15,
    "preview": "/*\n!.gitignore\n"
  },
  {
    "path": ".php-cs-fixer.php",
    "chars": 991,
    "preview": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->ignoreVCSIgnored(true)\n    ->ignoreDotFiles(false)\n    ->in(__DIR__)\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3714,
    "preview": "# CHANGELOG\n\n## 4.0.0 (not released yet)\n\n* Tooling\n  * Migrate from Invoke to Castor\n  * Add `castor symfony` to instal"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1936,
    "preview": "# Contributing\n\nFirst of all, **thank you** for contributing, **you are awesome**!\n\nEverybody should be able to help. He"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2019-present JoliCode\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.dist.md",
    "chars": 2970,
    "preview": "# My project\n\n## Running the application locally\n\n### Requirements\n\nA Docker environment is provided and requires you to"
  },
  {
    "path": "README.md",
    "chars": 35501,
    "preview": "<h1 align=\"center\">\n  <a href=\"https://github.com/jolicode/docker-starter\"><img src=\"https://jolicode.com/media/original"
  },
  {
    "path": "application/public/index.php",
    "chars": 194,
    "preview": "<?php\n\necho 'Hello world from PHP ', \\PHP_MAJOR_VERSION, '.', \\PHP_MINOR_VERSION, '.', \\PHP_RELEASE_VERSION, \" inside Do"
  },
  {
    "path": "castor.php",
    "chars": 4102,
    "preview": "<?php\n\nuse Castor\\Attribute\\AsTask;\n\nuse function Castor\\guard_min_version;\nuse function Castor\\import;\nuse function Cas"
  },
  {
    "path": "infrastructure/docker/docker-compose.dev.yml",
    "chars": 350,
    "preview": "services:\n    router:\n        build: services/router\n        volumes:\n            - \"/var/run/docker.sock:/var/run/docke"
  },
  {
    "path": "infrastructure/docker/docker-compose.yml",
    "chars": 3229,
    "preview": "# Templates to factorize the service definitions\nx-templates:\n    worker_base: &worker_base\n        build:\n            c"
  },
  {
    "path": "infrastructure/docker/services/php/Dockerfile",
    "chars": 4832,
    "preview": "# hadolint global ignore=DL3008\n\nFROM debian:12.8-slim AS php-base\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nRUN apt"
  },
  {
    "path": "infrastructure/docker/services/php/base/php-configuration/mods-available/app-default.ini",
    "chars": 557,
    "preview": "; priority=30\n[PHP]\nshort_open_tag = Off\nmemory_limit = 512M\nerror_reporting = E_ALL\ndisplay_errors = On\ndisplay_startup"
  },
  {
    "path": "infrastructure/docker/services/php/base/sudo.sh",
    "chars": 349,
    "preview": "#!/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"
  },
  {
    "path": "infrastructure/docker/services/php/builder/php-configuration/mods-available/app-builder.ini",
    "chars": 110,
    "preview": "; 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",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf",
    "chars": 2569,
    "preview": "user      nginx;\npid       /tmp/nginx.pid;\ndaemon    off;\nerror_log /proc/self/fd/2;\ninclude /etc/nginx/modules-enabled/"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/nginx/run",
    "chars": 32,
    "preview": "#!/bin/sh\n\nexec /usr/sbin/nginx\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore",
    "chars": 15,
    "preview": "/*\n!.gitignore\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/etc/service/php-fpm/run",
    "chars": 95,
    "preview": "#!/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",
    "chars": 15,
    "preview": "/*\n!.gitignore\n"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf",
    "chars": 321,
    "preview": "[global]\nerror_log  = /proc/self/fd/2\ndaemonize  = no\n\n[www]\nlisten = 127.0.0.1:9000\npm = dynamic\npm.max_children = 25\np"
  },
  {
    "path": "infrastructure/docker/services/php/frontend/php-configuration/mods-available/app-fpm.ini",
    "chars": 81,
    "preview": "; priority=40\n[PHP]\nexpose_php = off\nmemory_limit = 128M\nmax_execution_time = 30\n"
  },
  {
    "path": "infrastructure/docker/services/router/Dockerfile",
    "chars": 172,
    "preview": "FROM traefik:v3.6.1\n\nCOPY traefik /etc/traefik\n\nARG PROJECT_NAME\nRUN sed -i \"s/{{ PROJECT_NAME }}/${PROJECT_NAME}/g\" /et"
  },
  {
    "path": "infrastructure/docker/services/router/certs/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "infrastructure/docker/services/router/generate-ssl.sh",
    "chars": 343,
    "preview": "#!/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 -r"
  },
  {
    "path": "infrastructure/docker/services/router/openssl.cnf",
    "chars": 351,
    "preview": "# Configuration used in dev to generate a basic SSL cert\n[req]\ndefault_bits = 2048\nprompt = no\ndefault_md = sha256\ndisti"
  },
  {
    "path": "infrastructure/docker/services/router/traefik/dynamic_conf.yaml",
    "chars": 225,
    "preview": "tls:\n  stores:\n    default:\n      defaultCertificate:\n        certFile: /etc/ssl/certs/cert.pem\n        keyFile: /etc/ss"
  },
  {
    "path": "infrastructure/docker/services/router/traefik/traefik.yaml",
    "chars": 545,
    "preview": "global:\n  checkNewVersion: false\n  sendAnonymousUsage: false\n\nproviders:\n  docker:\n    exposedByDefault: false\n    const"
  },
  {
    "path": "phpstan.neon",
    "chars": 932,
    "preview": "parameters:\n    level: 8\n    paths:\n        # - application/src\n        - application/public\n        - castor.php\n      "
  },
  {
    "path": "tools/php-cs-fixer/.gitignore",
    "chars": 9,
    "preview": "/vendor/\n"
  },
  {
    "path": "tools/php-cs-fixer/composer.json",
    "chars": 243,
    "preview": "{\n    \"type\": \"project\",\n    \"require\": {\n        \"friendsofphp/php-cs-fixer\": \"^3.76.0\"\n    },\n    \"config\": {\n        "
  },
  {
    "path": "tools/phpstan/.gitignore",
    "chars": 15,
    "preview": "/var/\n/vendor/\n"
  },
  {
    "path": "tools/phpstan/composer.json",
    "chars": 468,
    "preview": "{\n    \"type\": \"project\",\n    \"require\": {\n        \"phpstan/extension-installer\": \"^1.4.3\",\n        \"phpstan/phpstan\": \"^"
  },
  {
    "path": "tools/twig-cs-fixer/.gitignore",
    "chars": 9,
    "preview": "/vendor/\n"
  },
  {
    "path": "tools/twig-cs-fixer/composer.json",
    "chars": 245,
    "preview": "{\n    \"type\": \"project\",\n    \"require\": {\n        \"vincentlanglet/twig-cs-fixer\": \"^3.8.1\"\n    },\n    \"config\": {\n      "
  }
]

About this extraction

This page contains the full source code of the jolicode/docker-starter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (94.2 KB), approximately 26.1k tokens, and a symbol index with 39 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!