diff --git a/.castor/docker.php b/.castor/docker.php index a97ec89..8f82342 100644 --- a/.castor/docker.php +++ b/.castor/docker.php @@ -343,18 +343,9 @@ function workers_stop(): void // So we find all services, in all profiles, and manually filter the one // that has the "worker" profile, then we stop it - - $services = json_decode( - docker_compose( - ['config', '--format', 'json'], - context()->withQuiet(), - profiles: ['*'], - )->getOutput(), - true, - )['services']; - $command = ['stop']; - foreach ($services as $name => $service) { + + foreach (get_services() as $name => $service) { foreach ($service['profiles'] ?? [] as $profile) { if ('worker' === $profile) { $command[] = $name; @@ -385,6 +376,7 @@ function docker_compose(array $subCommand, ?Context $c = null, array $profiles = 'PROJECT_DOMAINS' => $domains, 'USER_ID' => variable('user_id'), 'PHP_VERSION' => variable('php_version'), + 'REGISTRY' => variable('registry') ?? '', ]); $command = [ @@ -478,3 +470,104 @@ function run_in_docker_or_locally_for_mac(string $command, ?Context $c = null): docker_compose_run($command, c: $c); } } + +#[AsTask(description: 'Push images cache to the registry', namespace: 'docker', name: 'push', aliases: ['push'])] +function push(): 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')); + } + + // write bake file in tmp file + $bakeFile = tempnam(sys_get_temp_dir(), 'bake'); + file_put_contents($bakeFile, $content); + + // Run bake + run(['docker', 'buildx', 'bake', '-f', $bakeFile]); +} + +/** + * @return array, build: array{context: string, dockerfile?: string, cache_from?: list, target?: string}}> + */ +function get_services(): array +{ + return json_decode( + docker_compose( + ['config', '--format', 'json'], + context()->withQuiet(), + profiles: ['*'], + )->getOutput(), + true, + )['services']; +} diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml new file mode 100644 index 0000000..bd98582 --- /dev/null +++ b/.github/workflows/cache.yml @@ -0,0 +1,34 @@ +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.4" + +jobs: + push-images: + name: Push image to registry + runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - uses: actions/checkout@v4 + + - name: setup-castor + uses: castor-php/setup-castor@v0.1.0 + + - name: Log in to registry + shell: bash + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + + - name: "Build and start the infrastructure" + run: "castor docker:push" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebc8e36..0d22f1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,11 +10,13 @@ name: Continuous Integration 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: @@ -41,6 +43,10 @@ jobs: steps: - uses: actions/checkout@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@v0.1.0 diff --git a/README.md b/README.md index fb71804..ecd984d 100644 --- a/README.md +++ b/README.md @@ -1038,6 +1038,99 @@ and specifically [this commit](https://github.com/lyrixx/async-messenger-mercure +### How to use a docker registry to cache images layer + +
+ +Read the cookbook + +You can use a docker registry to cache images layer, it can be useful to speed up the build process during the CI and +local development. + +First you need a docker registry, in following examples we will use the GitHub registry (ghcr.io). + +Then add the registry to the context variable of the `castor.php` file: + +```php +function create_default_variables(): Context +{ + return [ + // [...] + 'registry' => 'ghcr.io/your-organization/your-project', + ]; +} +``` + +Once you have the registry, you can push the images to the registry: + +```bash +castor docker:push +``` + +> [!WARNING] Pushing images cache from a dev environment to a registry is not recommended, as cache is highly sensitive +> to the environment and may not be compatible with other environments. 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 +If you are using a GitHub action to build your images, you can use the cached images from the registry to speed up the build process. +However there are few steps to make it works nicely due to the docker binary limitations in GitHub actions. + +#### Pushing images to the registry from a GitHub action + +
+ +Read the cookbook + +To push images to the registry in a github action you will need to do this : + +1. Ensure that the github token have the `write:packages` scope. + +```yaml +permissions: + contents: read + packages: write +``` + + +2. Install Docker buildx in the github action + +```yaml + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 +``` + +3. Login to the registry + +```yaml + - name: Log in to registry + shell: bash + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin +``` + +#### Using the cached images in GitHub action + +By default images are private in the GitHub registry, you will need to login to the registry to pull the images. + +```yaml + - name: Log in to registry + shell: bash + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin +``` + +
+ ## Credits - Created at [JoliCode](https://jolicode.com/) diff --git a/castor.php b/castor.php index 2eee11e..bcf0fe4 100644 --- a/castor.php +++ b/castor.php @@ -33,7 +33,10 @@ function create_default_variables(): array '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.4', + 'registry' => $_SERVER['DS_REGISTRY'] ?? null, ]; } diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml index 68b6af1..8878717 100644 --- a/infrastructure/docker/docker-compose.yml +++ b/infrastructure/docker/docker-compose.yml @@ -38,6 +38,8 @@ services: build: context: services/php target: frontend + cache_from: + - "type=registry,ref=${REGISTRY:-}/frontend:cache" user: "${USER_ID}:${USER_ID}" volumes: - "../..:/var/www:cached" @@ -64,6 +66,8 @@ services: build: context: services/php target: builder + cache_from: + - "type=registry,ref=${REGISTRY:-}/builder:cache" init: true user: "${USER_ID}:${USER_ID}" environment: diff --git a/phpstan.neon b/phpstan.neon index dce53f8..fbfc932 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -28,5 +28,6 @@ parameters: power_shell: bool, user_id: int, root_dir: string, + registry?: ?string, } '''