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
}
}
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
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.