<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pascal Landau</title>
    <description>The latest articles on DEV Community by Pascal Landau (@pascallandau).</description>
    <link>https://dev.to/pascallandau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F853596%2F03771cea-d891-4c8b-8256-a3ef089ac899.jpg</url>
      <title>DEV Community: Pascal Landau</title>
      <link>https://dev.to/pascallandau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pascallandau"/>
    <language>en</language>
    <item>
      <title>A primer on GCP Compute Instance VMs for dockerized Apps [Tutorial Part 8]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Mon, 24 Apr 2023 12:14:13 +0000</pubDate>
      <link>https://dev.to/pascallandau/a-primer-on-gcp-compute-instance-vms-for-dockerized-apps-tutorial-part-8-4k46</link>
      <guid>https://dev.to/pascallandau/a-primer-on-gcp-compute-instance-vms-for-dockerized-apps-tutorial-part-8-4k46</guid>
      <description>&lt;p&gt;Getting started with the Google Cloud Platform (GCP) to run Virtual Machines (VMs) and prepare them to run dockerized applications.&lt;/p&gt;

&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/"&gt;A primer on GCP Compute Instance VMs for dockerized Apps [Tutorial Part 8]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In the eighth part of this tutorial series on developing PHP on Docker we will &lt;strong&gt;take a look on  the Google Cloud Platform (GCP)&lt;/strong&gt; and &lt;strong&gt;create a Compute Instance VM&lt;/strong&gt; to &lt;strong&gt;run dockerized  applications&lt;/strong&gt;. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating VMs&lt;/li&gt;
&lt;li&gt;using a container registry&lt;/li&gt;
&lt;li&gt;using a secret manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/RScVUaNHNxs"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/uPx9AZPOMrA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.   You find the branch with the final result of this tutorial at &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-8-gcp-compute-instance-vm-docker"&gt;part-8-gcp-compute-instance-vm-docker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/"&gt;Create a CI pipeline for dockerized PHP Apps&lt;/a&gt;. and the following one is &lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc"&gt;Deploy dockerized PHP Apps to production on GCP via docker compose&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Set up a GCP project&lt;/li&gt;
&lt;li&gt;
Create a service account

&lt;ul&gt;
&lt;li&gt;Create service account key file&lt;/li&gt;
&lt;li&gt;Configure IAM permissions&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Set up the &lt;code&gt;gcloud&lt;/code&gt; CLI tool&lt;/li&gt;
&lt;li&gt;
Set up the Container Registry

&lt;ul&gt;
&lt;li&gt;Authenticate docker&lt;/li&gt;
&lt;li&gt;Pushing images to the registry&lt;/li&gt;
&lt;li&gt;Images are stored in Google Cloud Storage buckets&lt;/li&gt;
&lt;li&gt;Pulling images from the registry&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Set up the Secret Manager

&lt;ul&gt;
&lt;li&gt;Create a secret via the UI&lt;/li&gt;
&lt;li&gt;View a secret via the UI&lt;/li&gt;
&lt;li&gt;Retrieve a secret via the &lt;code&gt;gcloud&lt;/code&gt; cli&lt;/li&gt;
&lt;li&gt;Add the secret &lt;code&gt;gpg&lt;/code&gt; key and password&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Compute Instances: The GCP VMs 

&lt;ul&gt;
&lt;li&gt;
Create a VM

&lt;ul&gt;
&lt;li&gt;General VM settings&lt;/li&gt;
&lt;li&gt;Firewall and networks tags&lt;/li&gt;
&lt;li&gt;The role of the service account&lt;/li&gt;
&lt;li&gt;Adding a public SSH key&lt;/li&gt;
&lt;li&gt;Define Availability Policies&lt;/li&gt;
&lt;li&gt;The actual VM creation&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Log into a VM

&lt;ul&gt;
&lt;li&gt;Login via SSH from the GCP UI&lt;/li&gt;
&lt;li&gt;Login via SSH with your own key from your host machine&lt;/li&gt;
&lt;li&gt;
Login using the Identity-Aware Proxy (IAP) concept

&lt;ul&gt;
&lt;li&gt;Additional notes on IAP&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Get &lt;code&gt;root&lt;/code&gt; permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; commands

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gcloud ssh --command=""&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gcloud scp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Provision the VM

&lt;ul&gt;
&lt;li&gt;Get the secret &lt;code&gt;gpg&lt;/code&gt; key and password from the Secret Manager&lt;/li&gt;
&lt;li&gt;Installing &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;docker compose&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Authenticate docker via &lt;code&gt;gcloud&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulling the &lt;code&gt;nginx&lt;/code&gt; image&lt;/li&gt;
&lt;li&gt;Start the &lt;code&gt;nginx&lt;/code&gt; container&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Automate via &lt;code&gt;gcloud&lt;/code&gt; commands

&lt;ul&gt;
&lt;li&gt;Preconditions: Project and &lt;code&gt;Owner&lt;/code&gt; service account&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;gcloud&lt;/code&gt; to use the master service account&lt;/li&gt;
&lt;li&gt;Enable APIs&lt;/li&gt;
&lt;li&gt;Create and configure a "deployment" service account&lt;/li&gt;
&lt;li&gt;Create secrets&lt;/li&gt;
&lt;li&gt;Create firewall rule for HTTP traffic&lt;/li&gt;
&lt;li&gt;Create a Compute Instance VM&lt;/li&gt;
&lt;li&gt;Provisioning&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Putting it all together &lt;/li&gt;
&lt;li&gt;Cleanup&lt;/li&gt;
&lt;li&gt;Wrapping up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the next tutorial we will &lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc"&gt;deploy our dockerized PHP Apps to "production" via docker compose&lt;/a&gt; and will create this "production" environment on &lt;strong&gt;GCP (Google Cloud Platform)&lt;/strong&gt;.  This tutorial serves as a primer on GCP to build up some fundamental knowledge, because we will  use the platform to provide all the &lt;strong&gt;infrastructure required to run our dockerized PHP  application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the process, we'll learn about GCP projects as our own "space"  in GCP and service accounts as a way to communicate  programmatically. We'll start by doing everything manually via the UI, but will also  explain how to do it programmatically via the &lt;code&gt;gcloud&lt;/code&gt; cli and  end with a fully automated script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-services.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a2XAfEsP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-services.PNG" alt="GCP Services" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The following video shows the overall flow&lt;/p&gt;


  
Your browser does not support the video tag.


&lt;p&gt;The API keys (see service account key files) that I use are &lt;br&gt;
&lt;strong&gt;not&lt;/strong&gt; in the repository, because I would be billed for any usage. I.e. &lt;strong&gt;you must create you own  project and keys&lt;/strong&gt; to follow along. &lt;/p&gt;


  
    &lt;strong&gt;Caution&lt;/strong&gt;
  
  
    Following the steps outlined in this tutorial &lt;strong&gt;will incur costs&lt;/strong&gt;, because we will 
    create "real" infrastructure. It won't be much (couple of cents), and it will very likely be 
    covered by the free 300$ grant that you get when trying out GCP (or the general unlimited 
    &lt;a href="https://cloud.google.com/free/docs/gcp-free-tier"&gt;GCP Free Tier&lt;/a&gt;
    ).&lt;br&gt; 
    &lt;br&gt;
    But you should still know about that upfront and &lt;strong&gt;make sure to shut everything down / 
    delete everything&lt;/strong&gt; in case you're trying it out yourself. The "safest" way to do so is
    Shutting down (deleting) the whole project
  




&lt;p&gt;&lt;a id="set-up-a-gcp-project"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Set up a GCP project
&lt;/h2&gt;

&lt;p&gt;On GCP, resources are organized under so-called  &lt;a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects"&gt;projects&lt;/a&gt;. We can  create a project via the &lt;a href="https://console.cloud.google.com/projectcreate"&gt;Create Project UI&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-project.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D9FwsBe---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-project.PNG" alt="Create a new GCP project" width="565" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;project ID&lt;/strong&gt; must be a globally unique string, and I have chosen &lt;code&gt;pl-dofroscra-p&lt;/code&gt; for this  tutorial (&lt;code&gt;pl&lt;/code&gt; =&amp;gt; Pascal Landau; &lt;code&gt;dofroscra&lt;/code&gt; =&amp;gt; Docker From Scratch; &lt;code&gt;p&lt;/code&gt; =&amp;gt; production).&lt;/p&gt;



&lt;p&gt;&lt;a id="create-a-service-account"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Create a service account
&lt;/h2&gt;

&lt;p&gt;As a next step, we need a &lt;a href="https://cloud.google.com/iam/docs/service-accounts"&gt;service account&lt;/a&gt; that we can &lt;strong&gt;use to make API requests&lt;/strong&gt;, because we don't want to use our "personal GCP account".  Service accounts are created via the  &lt;a href="https://console.cloud.google.com/iam-admin/serviceaccounts/create"&gt;IAM &amp;amp; Admin &amp;gt; Service Accounts UI&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-service-account.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ViqYTC8K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-service-account.PNG" alt="Create a new GCP service account" width="569" height="690"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;a id="create-service-account-key-file"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Create service account key file
&lt;/h3&gt;

&lt;p&gt;In order to &lt;strong&gt;use the account programmatically&lt;/strong&gt;, we also need to  &lt;a href="https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating"&gt;create a key file&lt;/a&gt; by choosing the "Manage Keys" option of the corresponding service account. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gqc98oGA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.PNG" alt="Create a new key file for a GCP service account UI" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will open up a UI at&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://console.cloud.google.com/iam-admin/serviceaccounts/details/$serviceAccountId/keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;$serviceAccountId&lt;/code&gt; is the numeric id of the service account, e.g. &lt;code&gt;109548647107864470967&lt;/code&gt;. To create a key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;click &lt;code&gt;"ADD KEY"&lt;/code&gt; and select &lt;code&gt;"Create new key"&lt;/code&gt; from the drop down menu

&lt;ul&gt;
&lt;li&gt;This will bring up a modal window to choose the key type. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;select the recommended JSON type and click &lt;code&gt;"Create"&lt;/code&gt;. 

&lt;ul&gt;
&lt;li&gt;GCP will then &lt;strong&gt;generate a new key pair&lt;/strong&gt;, store the public key and offer the private key file as 
download. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;download the file and make sure to treat it like any other private key (ssh, gpg, ...) 
i.e. &lt;strong&gt;never share it publicly&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.gif"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L67Ai_2E--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.gif" alt="Create a new key file for a GCP service account" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will &lt;strong&gt;store this file in the root of the codebase&lt;/strong&gt; at &lt;code&gt;gcp-service-account-key.json&lt;/code&gt; and add it  to the &lt;code&gt;.gitignore&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;Each service account has also a &lt;strong&gt;unique email address&lt;/strong&gt; that consists of its (non-numeric) &lt;code&gt;id&lt;/code&gt;  and the &lt;code&gt;project id&lt;/code&gt;. You can also find it directly in the key file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep "email" ./gcp-service-account-key.json
  "client_email": "docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This email address is usually used to reference the service account, e.g. when assigning IAM  permissions.&lt;/p&gt;

&lt;p&gt;&lt;a id="configure-iam-permissions"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure IAM permissions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;IAM&lt;/strong&gt; stands for &lt;a href="https://cloud.google.com/iam/docs#docs"&gt;Identity and Access Management (IAM)&lt;/a&gt;  and is used for &lt;strong&gt;managing permissions on GCP&lt;/strong&gt;. The  &lt;a href="https://cloud.google.com/iam/docs/understanding-roles"&gt;two core concepts are "permissions" and "roles"&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;permissions&lt;/strong&gt; are fine-grained for particular actions, e.g. &lt;code&gt;storage.buckets.create&lt;/code&gt; to "Create 
Cloud Storage buckets"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;roles&lt;/strong&gt; combine a selection of permissions, e.g. the &lt;code&gt;Cloud Storage Admin&lt;/code&gt; role has 
permissions like

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;storage.buckets.create&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storage.buckets.get&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;roles are assigned to &lt;strong&gt;users&lt;/strong&gt; (or service accounts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find a full overview of all permissions in the  &lt;a href="https://cloud.google.com/iam/docs/permissions-reference"&gt;Permissions Reference&lt;/a&gt; and all roles  under  &lt;a href="https://cloud.google.com/iam/docs/understanding-roles#predefined"&gt;Understanding roles &amp;gt; Predefined roles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this tutorial, we'll assign the following roles to the service account "user" &lt;code&gt;docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Storage Admin&lt;/code&gt; 

&lt;ul&gt;
&lt;li&gt;required to create the GCP bucket for the registry and to 
pull the images on the VM &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Secret Manager Admin&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;required to retrieve secrets from the Secret Manager
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Compute Admin&lt;/code&gt;, &lt;code&gt;Service Account User&lt;/code&gt; and &lt;code&gt;IAP-secured Tunnel User&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;are necessary for logging into a VM via IAP.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roles can be assigned through the &lt;a href="https://console.cloud.google.com/iam-admin/iam"&gt;Cloud Console IAM UI&lt;/a&gt; by editing the corresponding user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iam-permissions.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FjeGg271--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iam-permissions.PNG" alt="Managing IAM permissions" width="542" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt; It might take some time (usually a couple of seconds) until changes in IAM permissions take effect.&lt;/p&gt;

&lt;p&gt;&lt;a id="set-up-the-gcloud-cli-tool"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the &lt;code&gt;gcloud&lt;/code&gt; CLI tool
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://cloud.google.com/sdk/gcloud"&gt;CLI tool for GCP is called &lt;code&gt;gcloud&lt;/code&gt;&lt;/a&gt; and is &lt;a href="https://cloud.google.com/sdk/docs/install"&gt;available for all operating systems&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial we are using version &lt;code&gt;380.0.0&lt;/code&gt; &lt;strong&gt;installed natively on Windows&lt;/strong&gt; via the &lt;a href="https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe"&gt;GoogleCloudSDKInstaller.exe&lt;/a&gt; using the "Bundled Python" option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-installation-options.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ygZ5CGSe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-installation-options.PNG" alt="Install  raw `gcloud` endraw  on Windows" width="505" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FYI: As described under &lt;a href="https://cloud.google.com/sdk/docs/uninstall-cloud-sdk"&gt;Uninstalling the Google Cloud CLI&lt;/a&gt; you can find the installation and config directories via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# installation directory
$ gcloud info --format='value(installation.sdk_root)'
C:\Users\Pascal\AppData\Local\Google\Cloud SDK\google-cloud-sdk

# config directory
$ gcloud info --format='value(config.paths.global_config_dir)'
C:\Users\Pascal\AppData\Roaming\gcloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I will &lt;em&gt;not&lt;/em&gt; use my personal Google account to run &lt;code&gt;gcloud&lt;/code&gt; commands, thus I'm &lt;em&gt;not&lt;/em&gt; using the  "usual" initialization process &lt;a href="https://cloud.google.com/sdk/docs/initializing"&gt;by running &lt;code&gt;gcloud init&lt;/code&gt;&lt;/a&gt;. Instead, I will use the service account that we created previously and activate it as  described under &lt;a href="https://cloud.google.com/sdk/gcloud/reference/auth/activate-service-account"&gt;gcloud auth activate-service-account&lt;/a&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth activate-service-account docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com &lt;span class="nt"&gt;--key-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gcp-service-account-key.json &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud auth activate-service-account docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com --key-file=./gcp-service-account-key.json --project=pl-dofroscra-p
Activated service account credentials for: [docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI: Because we are using a &lt;code&gt;json&lt;/code&gt; key file that includes the service account ID, we can also  omit the id in the command, i.e.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud auth activate-service-account --key-file=./gcp-service-account-key.json --project=pl-dofroscra-p
Activated service account credentials for: [docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="set-up-the-container-registry"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the Container Registry
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/"&gt;We will use &lt;code&gt;docker compose&lt;/code&gt; to run our PHP application in the next tutorial part&lt;/a&gt; and need to &lt;strong&gt;make our docker images available&lt;/strong&gt; in a  &lt;a href="https://www.redhat.com/en/topics/cloud-native-apps/what-is-a-container-registry"&gt;container registry&lt;/a&gt;. Luckily, &lt;a href="https://cloud.google.com/container-registry"&gt;GCP offers a Container Registry product&lt;/a&gt;  that gives us a &lt;strong&gt;ready-to-use private registry as part of a GCP project&lt;/strong&gt;. Before we can use it,  the corresponding  &lt;a href="https://console.cloud.google.com/marketplace/product/google/containerregistry.googleapis.com"&gt;Google Container Registry API must be enabled&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-container-registry-api.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8_wCEtA0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-container-registry-api.PNG" alt="Enable the GCP Container Registry API" width="660" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You find the &lt;strong&gt;Container Registry&lt;/strong&gt; in the Cloud Console UI under  &lt;a href="https://console.cloud.google.com/gcr"&gt;Container Registry&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="authenticate-docker"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authenticate docker
&lt;/h3&gt;

&lt;p&gt;Since the Container Registry is private, we &lt;strong&gt;need to authenticate before we can push our  docker images&lt;/strong&gt;. The available authentication methods are described in the &lt;a href="https://cloud.google.com/container-registry/docs/advanced-authentication"&gt;GCP docu "Container Registry Authentication methods"&lt;/a&gt;. For pushing images from our local host system, we will use &lt;strong&gt;the service account key file&lt;/strong&gt; that we  created previously and run the command shown in the &lt;a href="https://cloud.google.com/container-registry/docs/advanced-authentication#json-key"&gt;"JSON key file" section&lt;/a&gt; of the the docu.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gcp-service-account-key.json
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | docker login &lt;span class="nt"&gt;-u&lt;/span&gt; _json_key &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; https://gcr.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful authentication looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat "$key" | docker login -u _json_key --password-stdin https://gcr.io
Login Succeeded

Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what exactly "happens" when we run this command? According to the  &lt;a href="https://docs.docker.com/engine/reference/commandline/login/"&gt;&lt;code&gt;docker login&lt;/code&gt;documentation&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you log in, the command stores credentials in &lt;code&gt;$HOME/.docker/config.json&lt;/code&gt; &lt;br&gt;
on Linux or &lt;code&gt;%USERPROFILE%/.docker/config.json&lt;/code&gt; on Windows&lt;br&gt;
[...]&lt;/p&gt;

&lt;p&gt;The Docker Engine can keep user credentials in an external credentials store, &lt;br&gt;
such as the native keychain of the operating system.&lt;br&gt;
[...]&lt;/p&gt;

&lt;p&gt;You need to specify the credentials store in &lt;br&gt;
&lt;code&gt;$HOME/.docker/config.json&lt;/code&gt; to tell the docker engine to use it.&lt;br&gt;
[...]&lt;/p&gt;

&lt;p&gt;By default, Docker looks for the native binary on each of the platforms, &lt;br&gt;
i.e. “osxkeychain” on macOS, “wincred” on windows, and “pass” on Linux.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words: I &lt;strong&gt;won't be able to see the content of the service account key file in "plain  text"&lt;/strong&gt; anywhere but docker will utilize the OS specific tools to store them securely. After I ran  the &lt;code&gt;docker login&lt;/code&gt; command on Windows, I found the following content in &lt;code&gt;~/.docker/config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat ~/.docker/config.json
{
        "auths": {
                "gcr.io": {}
        },
        "credsStore": "desktop"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI: &lt;code&gt;"desktop"&lt;/code&gt; seems to be a  &lt;a href="https://forums.docker.com/t/docker-windows-desktop-credentials-location/107251"&gt;wrapper for the Wincred executable&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="pushing-images-to-the-registry"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pushing images to the registry
&lt;/h3&gt;

&lt;p&gt;For this tutorial, we will create a super simple &lt;code&gt;nginx&lt;/code&gt; alpine image that provides a "Hello  world" &lt;code&gt;hello.html&lt;/code&gt; file via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-nginx &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
FROM nginx:1.21.5-alpine

RUN echo "Hello world" &amp;gt;&amp;gt; /usr/share/nginx/html/hello.html
&lt;/span&gt;&lt;span class="no"&gt;
EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name of the image is &lt;code&gt;my-nginx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker image ls | grep my-nginx
my-nginx         latest             42dd1608d126   50 seconds ago    23.5MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to  &lt;a href="https://docs.docker.com/engine/reference/commandline/push/"&gt;push an image to a registry&lt;/a&gt;,&lt;br&gt;
&lt;strong&gt;the image name must be prefixed with the corresponding registry&lt;/strong&gt;. This was quite confusing to  me, because I would have expected to be able to run something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker push my-nginx &lt;span class="nt"&gt;--registry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gcr.io

unknown flag: &lt;span class="nt"&gt;--registry&lt;/span&gt;
See &lt;span class="s1"&gt;'docker push --help'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But nope, there is no such &lt;code&gt;--registry&lt;/code&gt; option. Even worse: &lt;strong&gt;Omitting it would cause a push to  &lt;code&gt;docker.io&lt;/code&gt;&lt;/strong&gt;, the "default" registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker push my-nginx
Using default tag: latest
The push refers to repository [docker.io/my-nginx]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to  &lt;a href="https://cloud.google.com/container-registry/docs/pushing-and-pulling"&gt;the GCP docs on Pushing and pulling images&lt;/a&gt;,  the following steps are necessary to push an image to a GCP registry:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tag&lt;/strong&gt; the image with its target path in Container Registry, including the gcr.io registry 
host and the project ID my-project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push&lt;/strong&gt; the image to the registry&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our case &lt;strong&gt;the target path to our Container Registry&lt;/strong&gt; is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcr.io/pl-dofroscra-p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because &lt;code&gt;pl-dofroscra-p&lt;/code&gt; is the id of the GCP project we created previously.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;full image name&lt;/strong&gt; becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcr.io/pl-dofroscra-p/my-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To push the &lt;code&gt;my-nginx&lt;/code&gt; image, we must first &lt;strong&gt;"add another name"&lt;/strong&gt; to it via  &lt;a href="https://docs.docker.com/engine/reference/commandline/tag/"&gt;&lt;code&gt;docker tag&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker tag my-nginx gcr.io/pl-dofroscra-p/my-nginx

$ docker image ls
REPOSITORY                       TAG                IMAGE ID       CREATED          SIZE
my-nginx                         latest             ba7a2c5faf0d   15 minutes ago   23.5MB
gcr.io/pl-dofroscra-p/my-nginx   latest             ba7a2c5faf0d   15 minutes ago   23.5MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;strong&gt;push that name afterwards&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker push gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
The push refers to repository [gcr.io/pl-dofroscra-p/my-nginx]
134174afa9ad: Preparing
cb7b4430c52d: Preparing
419df8b60032: Preparing
0e835d02c1b5: Preparing
5ee3266a70bd: Preparing
3f87f0a06073: Preparing
1c9c1e42aafa: Preparing
8d3ac3489996: Preparing
8d3ac3489996: Waiting
3f87f0a06073: Waiting
1c9c1e42aafa: Waiting
cb7b4430c52d: Pushed
134174afa9ad: Pushed
419df8b60032: Pushed
5ee3266a70bd: Pushed
0e835d02c1b5: Pushed
8d3ac3489996: Layer already exists
3f87f0a06073: Pushed
1c9c1e42aafa: Pushed
latest: digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f size: 1982
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then find the image in the  &lt;a href="https://console.cloud.google.com/gcr"&gt;UI of the Container Registry&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-container-registry-image-example.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uAXXHoQA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-container-registry-image-example.PNG" alt="Example of a pushed image in the Container Registry" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't worry: We won't have to do the tagging every time before a push, because we will  &lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/#adding-gcp-values-to-make-variables-env"&gt;set up &lt;code&gt;make&lt;/code&gt; to use the correct name automatically&lt;/a&gt; when building the images in the &lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/"&gt;next part&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="images-are-stored-in-google-cloud-storage-buckets"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Images are stored in Google Cloud Storage buckets
&lt;/h3&gt;

&lt;p&gt;We assigned the &lt;code&gt;Storage Admin&lt;/code&gt; role to the service account previously that contains the &lt;code&gt;storage.buckets.create&lt;/code&gt; permission. If we wouldn't have done that, the  following error would have occurred:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;denied: Token exchange failed for project 'pl-dofroscra-p'. Caller does not have permission 'storage.buckets.create'. To configure permissions, follow instructions at: https://cloud.google.com/container-registry/docs/access-control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Container Registry tries to &lt;strong&gt;store the docker images in a Google Cloud Storage bucket&lt;/strong&gt; that  is created on the fly when the &lt;strong&gt;very first image is pushed&lt;/strong&gt;, see  &lt;a href="https://cloud.google.com/container-registry/docs/pushing-and-pulling#add-registry"&gt;the GCP docs on "Adding a registry"&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The first image push to a hostname triggers creation of the registry in a project &lt;br&gt;
and the corresponding Cloud Storage storage bucket. &lt;br&gt;
This initial push requires project-wide permissions to create storage buckets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can find the bucket, that in my case is named &lt;code&gt;artifacts.pl-dofroscra-p.appspot.com&lt;/code&gt; in the &lt;a href="https://console.cloud.google.com/storage"&gt;Cloud Storage UI&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-cloud-storage-registry-images.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GfCvljdr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-cloud-storage-registry-images.PNG" alt="GCP Container Registry image location on Cloud Storage" width="745" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: Make sure to &lt;strong&gt;delete this bucket once you are done with the tutorial&lt;/strong&gt; - otherwise  &lt;a href="https://cloud.google.com/storage/pricing"&gt;storage costs&lt;/a&gt; will incur.&lt;/p&gt;

&lt;p&gt;&lt;a id="pulling-images-from-the-registry"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pulling images from the registry
&lt;/h3&gt;

&lt;p&gt;According to the  &lt;a href="https://cloud.google.com/container-registry/docs/pushing-and-pulling#pulling_images_from_a_registry"&gt;GCP docs to pull an image from the Container Registry&lt;/a&gt; we need to be authenticated with a user that  has the permissions of the  &lt;a href="https://cloud.google.com/storage/docs/access-control/iam-roles#standard-roles"&gt;&lt;code&gt;Storage Object Viewer&lt;/code&gt;&lt;/a&gt;  role to access the "raw" images. FYI: The &lt;code&gt;Storage Admin&lt;/code&gt; role that we assigned previously has all  the permissions of the &lt;code&gt;Storage Object Viewer&lt;/code&gt; role.&lt;/p&gt;

&lt;p&gt;Then use the &lt;strong&gt;fully qualified image name&lt;/strong&gt; as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull gcr.io/pl-dofroscra-p/my-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output if the image is cached&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
Digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f
Status: Image is up to date for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or if doesn't exist&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
59bf1c3509f3: Pull complete 
f3322597df46: Pull complete 
d09cf91cabdc: Pull complete 
3a97535ac2ef: Pull complete 
919ade35f869: Pull complete 
40e5d2fe5bcd: Pull complete 
c72acb0c83a5: Pull complete 
d6baa2bee4a5: Pull complete 
Digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f
Status: Downloaded newer image for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="set-up-the-secret-manager"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the Secret Manager
&lt;/h2&gt;

&lt;p&gt;Even though  &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/"&gt;we use &lt;code&gt;git secret&lt;/code&gt; to manage our secrets&lt;/a&gt;, &lt;strong&gt;we  still need the &lt;code&gt;gpg&lt;/code&gt; secret key&lt;/strong&gt; for decryption. This key is "a secret in itself" and we will use  the &lt;a href="https://cloud.google.com/secret-manager/docs"&gt;GCP Secret Manager&lt;/a&gt; to store it and retrieve it later from a VM.&lt;/p&gt;

&lt;p&gt;It can be managed from the &lt;a href="https://console.cloud.google.com/security/secret-manager"&gt;Security &amp;gt; Secret Manager UI&lt;/a&gt;  once we have &lt;a href="https://console.cloud.google.com/marketplace/product/google/secretmanager.googleapis.com"&gt;enabled the Secret Manager API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-secret-manager-api.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WOyAKV-x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-secret-manager-api.PNG" alt="Enable the GCP Secret Manager API" width="663" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="create-a-secret-via-the-ui"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a secret via the UI
&lt;/h3&gt;

&lt;p&gt;To create a secret:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;navigate to the &lt;a href="https://console.cloud.google.com/security/secret-manager"&gt;Secret Manager UI&lt;/a&gt; 
and click the &lt;code&gt;"+ CREATE SECRET"&lt;/code&gt; button&lt;/li&gt;
&lt;li&gt;enter the &lt;strong&gt;secret name&lt;/strong&gt; and &lt;strong&gt;secret value&lt;/strong&gt; in the form (we can ignore the other advanced 
settings like "Replication policy" and "Encryption" for now)

&lt;ul&gt;
&lt;li&gt;FYI: the secret name can only contain English letters (A-Z), numbers (0-9), dashes (-), and 
underscores (_)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;click the &lt;code&gt;"CREATE SECRET"&lt;/code&gt; button&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following gif shows the creation of a secret named &lt;code&gt;my_secret_key&lt;/code&gt; with the value  &lt;code&gt;my_secret_value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-and-view-secret.gif"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9d7yp4KR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-and-view-secret.gif" alt="Create and view a secret" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="view-a-secret-via-the-ui"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  View a secret via the UI
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://console.cloud.google.com/security/secret-manager"&gt;Security &amp;gt; Secret Manager UI&lt;/a&gt;  lists all existing secrets. Clicking on a secret will lead you to the &lt;strong&gt;Secret Detail UI&lt;/strong&gt; at the URL&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://console.cloud.google.com/security/secret-manager/secret/$secretName/versions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;e.g. for secret name &lt;code&gt;my_secret_key&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://console.cloud.google.com/security/secret-manager/secret/my_secret_key/versions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI shows all &lt;a href="https://cloud.google.com/secret-manager/docs/managing-secret-versions"&gt;versions of the secret&lt;/a&gt;, though we currently only have one. To view the actual secret value, click on the three dots in  the "Actions" column and select "View secret value" (see gif in the  previous section).&lt;/p&gt;

&lt;p&gt;&lt;a id="retrieve-a-secret-via-the-gcloud-cli"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieve a secret via the &lt;code&gt;gcloud&lt;/code&gt; cli
&lt;/h3&gt;

&lt;p&gt;To retrieve a secret with a service account via the &lt;code&gt;gcloud&lt;/code&gt; cli, it needs the permission  &lt;code&gt;secretmanager.versions.access&lt;/code&gt;, that is part of the  &lt;a href="https://cloud.google.com/iam/docs/understanding-roles#secret-manager-roles"&gt;&lt;code&gt;Secret Manager Secret Accessor&lt;/code&gt;&lt;/a&gt;  role (as well as the &lt;code&gt;Secret Manager Admin&lt;/code&gt; role). Let's first show all available secrets via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets list
NAME           CREATED              REPLICATION_POLICY  LOCATIONS
my_secret_key  2022-05-15T05:38:11  automatic           -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To "see" the value of &lt;code&gt;my_secret_key&lt;/code&gt;, we must also define the corresponding version. All  versions can be listed via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets versions list my_secret_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets versions list my_secret_key
NAME  STATE    CREATED              DESTROYED
1     enabled  2022-05-15T05:38:13  -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual secret value for version &lt;code&gt;1&lt;/code&gt; of &lt;code&gt;my_secret_key&lt;/code&gt; is accessed via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets versions access 1 &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my_secret_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets versions access 1 --secret=my_secret_key
my_secret_value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use  &lt;a href="https://cloud.google.com/sdk/gcloud/reference/secrets/versions/access"&gt;the string &lt;code&gt;latest&lt;/code&gt; as version to retrieve the latest enabled version&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Version resource - Numeric secret version to access or a configured alias &lt;br&gt;
(including 'latest' to use the latest version).&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my_secret_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets versions access latest --secret=my_secret_key
my_secret_value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="add-the-secret-gpg-key-and-password"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the secret &lt;code&gt;gpg&lt;/code&gt; key and password
&lt;/h3&gt;

&lt;p&gt;We already know how to &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#create-gpg-key-pair"&gt;create another &lt;code&gt;gpg&lt;/code&gt; key pair&lt;/a&gt; and  &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#adding-listing-and-removing-users"&gt;add the corresponding email to &lt;code&gt;git secret&lt;/code&gt;&lt;/a&gt; from when we have set up the CI pipelines (see section  &lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/#add-a-password-protected-secret-gpg-key"&gt;Add a password-protected secret gpg key&lt;/a&gt;) . We will do the same once more for the production environment via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Production Deployment"&lt;/span&gt;
&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"production@example.com"&lt;/span&gt;
&lt;span class="nv"&gt;passphrase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;87654321
&lt;span class="nv"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret-production-protected.gpg.example
&lt;span class="nv"&gt;public&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.dev/gpg-keys/production-public.gpg
&lt;span class="c"&gt;# export key pair&lt;/span&gt;
gpg &lt;span class="nt"&gt;--batch&lt;/span&gt; &lt;span class="nt"&gt;--gen-key&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="sh"&gt;
Name-Email: &lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="sh"&gt;
Expire-Date: 0
Passphrase: &lt;/span&gt;&lt;span class="nv"&gt;$passphrase&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# export the private key&lt;/span&gt;
gpg &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="nt"&gt;--pinentry-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;loopback &lt;span class="nt"&gt;--passphrase&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$passphrase&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;--export-secret-key&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;

&lt;span class="c"&gt;# export the public key&lt;/span&gt;
gpg &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;--export&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$public&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find the secret key in the codebase at &lt;code&gt;secret-production-protected.gpg.example&lt;/code&gt;  and the public key at &lt;code&gt;.dev/gpg-keys/production-public.gpg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I have also added the secret &lt;code&gt;gpg&lt;/code&gt; key and password as secrets to the secret manager&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-gpg-secret-password.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wz9wanhs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-gpg-secret-password.PNG" alt="GPG secret key and password stored as secrets" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="compute-instances-the-gcp-vms"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compute Instances: The GCP VMs
&lt;/h2&gt;

&lt;p&gt;Compute Instances are the equivalent of &lt;a href="https://aws.amazon.com/de/ec2/"&gt;AWS EC2 instances&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To run our application, &lt;strong&gt;we need a VM with a public IP address&lt;/strong&gt; so that it can be reached from the  internet. VMs on GCP are called &lt;a href="https://cloud.google.com/compute"&gt;Compute Instances&lt;/a&gt; and can be  managed from the &lt;a href="https://console.cloud.google.com/compute"&gt;Compute Instance UI&lt;/a&gt; - though we  must first  &lt;a href="https://console.cloud.google.com/marketplace/product/google/compute.googleapis.com"&gt;activate the Compute Instance API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-compute-instance-api.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Bpru4QwS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-compute-instance-api.PNG" alt="Enable the GCP Compute Instance API" width="518" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="create-a-vm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a VM
&lt;/h3&gt;

&lt;p&gt;We can simply create a new instance from the  &lt;a href="https://console.cloud.google.com/compute/instancesAdd"&gt;Create an instance UI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="general-vm-settings"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  General VM settings
&lt;/h4&gt;

&lt;p&gt;We'll use the following settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;dofroscra-test&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Region &lt;code&gt;us-central1 (Iowa)&lt;/code&gt; and Zone &lt;code&gt;us-central1-a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Machine family: &lt;code&gt;General Purpose &amp;gt; E2 &amp;gt; e2-micro (2 vCPU, 1 GB memory)&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Boot Disk: Debian GNU/Linux 11 (bullseye); 10 GB&lt;/li&gt;
&lt;li&gt;Identity and API access: Choose the "Docker PHP Tutorial deployment account" service account 
that we created previously &lt;/li&gt;
&lt;li&gt;Firewall: Allow HTTP traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IlM3ppzx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings.PNG" alt="GCP instance settings" width="536" height="796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-service-account-firewall.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0jJr2BA_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-service-account-firewall.PNG" alt="Additional GCP instance settings" width="614" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="firewall-and-networks-tags"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Firewall and networks tags
&lt;/h4&gt;

&lt;p&gt;Ticking the &lt;code&gt;Allow HTTP traffic&lt;/code&gt; checkbox will cause two things when the instance is created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new &lt;strong&gt;firewall rule&lt;/strong&gt; named &lt;code&gt;default-allow-http&lt;/code&gt; is created that allows incoming traffic from 
port &lt;code&gt;80&lt;/code&gt; and is only applied to instances with the 
&lt;a href="https://cloud.google.com/vpc/docs/add-remove-network-tags"&gt;network tag&lt;/a&gt; &lt;code&gt;http-server&lt;/code&gt;. You 
can see the new rule in the 
&lt;a href="https://console.cloud.google.com/networking/firewalls/list"&gt;Firewall UI&lt;/a&gt;
&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-firewall-rule-default-allow-http.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--p2DtBTZH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-firewall-rule-default-allow-http.PNG" alt="Firewall rule: default-allow-http" width="767" height="265"&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;network tag&lt;/strong&gt; &lt;code&gt;http-server&lt;/code&gt; is added to the instance, effectively enabling the 
aforementioned firewall rule for the instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="the-role-of-the-service-account"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The role of the service account
&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;service account&lt;/strong&gt; that we have chosen in the previous step &lt;strong&gt;will be attached to the  Compute Instance&lt;/strong&gt;. I.e. it will be available on the instance and &lt;strong&gt;we will have access to the account when we log into the instance&lt;/strong&gt;. Conveniently,  &lt;a href="https://cloud.google.com/sdk/docs/install-sdk#installing_the_latest_version"&gt;the&lt;code&gt;gcloud&lt;/code&gt; cli is pre-installed on every Compute Instance&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're using an instance on Compute Engine, the gcloud CLI is installed by default.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-cli-preinstalled-compute-instance.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--44Xjcjt6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-cli-preinstalled-compute-instance.PNG" alt="The raw `gcloud` endraw  cli is pre-installed on every Compute Instance" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The exact rules for &lt;strong&gt;what the service account can do&lt;/strong&gt; are described in the  &lt;a href="https://cloud.google.com/compute/docs/access/service-accounts"&gt;Compute Engine docs for "Service accounts"&lt;/a&gt;. In short: The possible actions will be constrained by the  &lt;a href="https://cloud.google.com/compute/docs/access/service-accounts#usingroles"&gt;IAM permissions of the service account&lt;/a&gt; and the &lt;a href="https://cloud.google.com/compute/docs/access/service-accounts#accesscopesiam"&gt;Access scopes of the Compute Instance&lt;/a&gt;  which are set as outlined in the  &lt;a href="https://cloud.google.com/compute/docs/access/service-accounts#default_scopes"&gt;"Default scopes" section&lt;/a&gt;. Just take this as an aside, we won't have to modify anything here.&lt;/p&gt;

&lt;p&gt;We will use the service account later,  when we log into the Compute Instance and run &lt;code&gt;docker pull&lt;/code&gt; from there to pull images from the registry.&lt;/p&gt;

&lt;p&gt;&lt;a id="adding-a-public-ssh-key"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding a public SSH key
&lt;/h4&gt;

&lt;p&gt;In addition, I &lt;strong&gt;added my own public &lt;code&gt;ssh&lt;/code&gt; key&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-rsa AAAAB3NzaC1yc2....6row== pascal.landau@MY_LAPTOP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: The &lt;strong&gt;username&lt;/strong&gt; for this key will be defined &lt;strong&gt;at the end of the key&lt;/strong&gt;! E.g. in the  example above, the username would be &lt;code&gt;pascal.landau&lt;/code&gt;. This is important for  logging in later via SSH from your local machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-ssh-key.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--H49BYkLR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-ssh-key.PNG" alt="Add SSH key to GCP instance settings" width="597" height="761"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="define-availability-policies"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Define Availability Policies
&lt;/h4&gt;

&lt;p&gt;We will make the instance &lt;a href="https://cloud.google.com/compute/docs/instances/preemptible"&gt;preemptible&lt;/a&gt;, by choosing the &lt;strong&gt;VM provisioning model&lt;/strong&gt; &lt;code&gt;Spot&lt;/code&gt;. This makes it &lt;strong&gt;much cheaper&lt;/strong&gt; but GCP "might" &lt;strong&gt;terminate the instance randomly&lt;/strong&gt; if the  capacity is needed somewhere else (and definitely after 24 hours). This is completely fine for our  test use case. The final costs are ~2$ per month for this instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-availability-policies.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L1DARaNg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-availability-policies.PNG" alt="Define Availability Policies" width="598" height="864"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="the-actual-vm-creation"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The actual VM creation
&lt;/h4&gt;

&lt;p&gt;Finally, click the &lt;code&gt;"Create"&lt;/code&gt; button at the bottom of the page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-create.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AdkDBgHq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-create.PNG" alt="Create the GCP instance" width="634" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the instance is created, you can see it in the  &lt;a href="https://console.cloud.google.com/compute/instances"&gt;Compute Instances &amp;gt; VM instances UI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-overview.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4auswXi1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-overview.PNG" alt="Compute Instance overview" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next to the instance name &lt;code&gt;dofroscra-test&lt;/code&gt; we can see the external IP address &lt;code&gt;35.192.212.130&lt;/code&gt;  Opening it in a browser via &lt;code&gt;http://35.192.212.130/&lt;/code&gt; won't show anything though, because we  didn't deploy the application yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: Make sure to &lt;strong&gt;shutdown and remove this instance by the end of the tutorial&lt;/strong&gt; - otherwise &lt;a href="https://cloud.google.com/compute/all-pricing"&gt;compute costs&lt;/a&gt; will incur.&lt;/p&gt;

&lt;p&gt;&lt;a id="log-into-a-vm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Log into a VM
&lt;/h3&gt;

&lt;p&gt;There are multiple ways &lt;strong&gt;to log into the VM / Compute Instance&lt;/strong&gt; outlined in  &lt;a href="https://cloud.google.com/compute/docs/instances/connecting-advanced"&gt;Connecting to Linux VMs using advanced methods&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm going to describe three of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login via SSH from the GCP UI&lt;/li&gt;
&lt;li&gt;Login via SSH with your own key from your host machine&lt;/li&gt;
&lt;li&gt;Login using the Identity-Aware Proxy (IAP) concept&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PS: It's worth keeping  &lt;a href="https://cloud.google.com/compute/docs/troubleshooting/troubleshooting-ssh"&gt;the Troubleshooting SSH guide&lt;/a&gt; as a bookmark.&lt;/p&gt;

&lt;p&gt;&lt;a id="login-via-ssh-from-the-gcp-ui"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Login via SSH from the GCP UI
&lt;/h4&gt;

&lt;p&gt;Probably the easiest way to log in: Simply &lt;strong&gt;click the &lt;code&gt;"SSH"&lt;/code&gt; button&lt;/strong&gt; in the &lt;a href="https://console.cloud.google.com/compute/instances"&gt;Compute Instances &amp;gt; VM instances UI&lt;/a&gt; &lt;strong&gt;next  to the instance you want to log in&lt;/strong&gt;. This will &lt;strong&gt;create a web shell&lt;/strong&gt; that uses an ephemeral SSH key according to the &lt;a href="https://cloud.google.com/compute/docs/instances/connecting-to-instance#connect_to_vms"&gt;GCP documentation: Connect to Linux VMs &amp;gt; Connect to VMs&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you connect to VMs using the Cloud Console, Compute Engine creates an ephemeral SSH key for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-web-shell.gif"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nJi6n0OS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-web-shell.gif" alt="Connect via Cloud Console UI" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="login-via-ssh-with-your-own-key-from-your-host-machine"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Login via SSH with your own key from your host machine
&lt;/h4&gt;

&lt;p&gt;This method is probably closest to what you are used to from working with "other" VMs. In this  case, &lt;strong&gt;the instance has to be publicly available&lt;/strong&gt; (i.e. reachable "from the internet") and &lt;br&gt;
&lt;strong&gt;expose a port for SSH connections&lt;/strong&gt; (usually &lt;code&gt;22&lt;/code&gt;). In addition, your public SSH key needs to be  deployed.&lt;/p&gt;

&lt;p&gt;All of those requirements are true  for the Compute Instance that we just created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the public ip address is &lt;code&gt;35.192.212.130&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;port &lt;code&gt;22&lt;/code&gt; is open by default via the &lt;code&gt;default-allow-ssh&lt;/code&gt; firewall rule, see
&lt;a href="https://geekflare.com/gcp-firewall-configuration/"&gt;How to Configure Firewall Rules in Google Cloud Platform&lt;/a&gt;
and the &lt;a href="https://cloud.google.com/compute/docs/troubleshooting/troubleshooting-ssh"&gt;official documentation on Troubleshooting SSH&lt;/a&gt;
&amp;gt; By default, Compute Engine VMs allow SSH access on port 22.&lt;/li&gt;
&lt;li&gt;we added our public SSH key using &lt;code&gt;pascal.landau&lt;/code&gt; as the username&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we can now simply login via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh pascal.landau@35.192.212.130
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or if you need to specify the location of the private key via the &lt;code&gt;-i&lt;/code&gt; option&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_rsa pascal.landau@35.192.212.130
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ssh pascal.landau@35.192.212.130
Linux dofroscra-test 4.19.0-20-cloud-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Apr 10 16:21:00 2022 from 54.74.228.207
pascal.landau@dofroscra-test:~$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="login-using-the-identity-aware-proxy-iap-concept"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Login using the Identity-Aware Proxy (IAP) concept
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Note: This is the preferred way of logging into a GCP VM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;login via IAP&lt;/strong&gt; we need the &lt;code&gt;gcloud&lt;/code&gt; CLI that will &lt;strong&gt;use API  requests (via HTTPS) under the hood to authenticate the Google user&lt;/strong&gt; (or in our case: the  service account) and then proxy the requests to the VM via &lt;a href="https://cloud.google.com/iap/docs/concepts-overview"&gt;GCP's Identity-Aware Proxy (IAP)&lt;/a&gt; as described in &lt;a href="https://cloud.google.com/compute/docs/instances/connecting-advanced#cloud_iap"&gt;Connecting through Identity-Aware Proxy (IAP) for TCP&lt;/a&gt; and in more detail under &lt;a href="https://cloud.google.com/iap/docs/using-tcp-forwarding#tunneling_ssh_connections"&gt;Using IAP for TCP forwarding &amp;gt; Tunneling SSH connections&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The corresponding command is &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh"&gt;&lt;code&gt;gcloud compute ssh&lt;/code&gt;&lt;/a&gt;, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Note the &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--tunnel-through-iap"&gt;--tunnel-through-iap&lt;/a&gt;  flag: Without it, &lt;code&gt;gcloud&lt;/code&gt; would instead attempt to use a "normal" &lt;code&gt;ssh&lt;/code&gt; connection if the VM is  publicly reachable  &lt;a href="https://cloud.google.com/iap/docs/using-tcp-forwarding#tunneling_ssh_connections"&gt;as per docu&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the instance doesn't have an external IP address, the connection automatically uses &lt;br&gt;
IAP TCP tunneling. If the instance does have an external IP address, the connection uses the &lt;br&gt;
external IP address instead of IAP TCP tunneling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p
WARNING: The private SSH key file for gcloud does not exist.
WARNING: The public SSH key file for gcloud does not exist.
WARNING: The PuTTY PPK SSH key file for gcloud does not exist.
WARNING: You do not have an SSH key for gcloud.
WARNING: SSH keygen will be executed to generate a key.
Updating project ssh metadata...
..............................................Updated [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p].
.done.
Waiting for SSH key to propagate.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-iap.gif"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lFmmyTTK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-iap.gif" alt="Connect via IAP" width="766" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, this command will &lt;strong&gt;automatically create an SSH key pair&lt;/strong&gt; on your local machine under  &lt;code&gt;~/.ssh/&lt;/code&gt; named &lt;code&gt;google_compute_engine&lt;/code&gt; (unless they already exist) and upload the public key  to the instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ls -l ~/.ssh/ | grep google_compute_engine
-rw-r--r-- 1 Pascal 197121 1675 Apr 11 09:25 google_compute_engine
-rw-r--r-- 1 Pascal 197121 1456 Apr 11 09:25 google_compute_engine.ppk
-rw-r--r-- 1 Pascal 197121  420 Apr 11 09:25 google_compute_engine.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also opens a &lt;a href="https://www.putty.org/"&gt;&lt;code&gt;Putty&lt;/code&gt; session&lt;/a&gt; and logs you into in the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using username "Pascal".
Authenticating with public key "LAPTOP-0DNL2Q02\Pascal@LAPTOP-0DNL2Q02"
Linux dofroscra-test 4.19.0-20-cloud-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Apr 11 07:22:32 2022 from 54.74.228.207
Pascal@dofroscra-test:~$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caution&lt;/strong&gt;: The &lt;code&gt;Putty&lt;/code&gt; session will be closed automatically when you abort the original  &lt;code&gt;gcloud compute ssh&lt;/code&gt; command!&lt;/p&gt;

&lt;p&gt;&lt;a id="additional-notes-on-iap"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Additional notes on IAP
&lt;/h5&gt;

&lt;p&gt;In order to &lt;strong&gt;enable SSH connections via IAP&lt;/strong&gt;, our service account needs the following  IAM roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/compute/docs/access/iam#compute.instanceAdmin.v1"&gt;&lt;code&gt;roles/compute.instanceAdmin.v1&lt;/code&gt;&lt;/a&gt;, role: &lt;code&gt;Compute Admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/compute/docs/access/iam#the_serviceaccountuser_role"&gt;&lt;code&gt;roles/iam.serviceAccountUser&lt;/code&gt;&lt;/a&gt;, role: &lt;code&gt;Service Account User&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/iam/docs/understanding-roles#cloud-iap-roles"&gt;&lt;code&gt;roles/iap.tunnelResourceAccessor&lt;/code&gt;&lt;/a&gt;, role: &lt;code&gt;IAP-secured Tunnel User&lt;/code&gt;
Otherwise, you might run into a couple of errors like:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: (gcloud.compute.ssh) Could not fetch resource:
 - Required 'compute.instances.get' permission for 'projects/pl-dofroscra-p/zones/us-central1-a/instances/dofroscra-test'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 (&lt;code&gt;roles/compute.instanceAdmin.v1&lt;/code&gt; missing)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: (gcloud.compute.ssh) Could not add SSH key to instance metadata:
 - The user does not have access to service account 'docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com'.  User: 'docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com'.  Ask a project owner to grant you the iam.serviceAccountUser role on the service account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 (&lt;a href="https://cloud.google.com/compute/docs/access/iam#the_serviceaccountuser_role"&gt;&lt;code&gt;roles/iam.serviceAccountUser&lt;/code&gt; missing&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Remote side unexpectedly closed connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 (&lt;code&gt;roles/iap.tunnelResourceAccessor&lt;/code&gt; missing)&lt;/p&gt;

&lt;p&gt;Note: This error can also occur if you &lt;strong&gt;try to use IAP directly after starting a VM&lt;/strong&gt;. In this  case wait a couple of seconds and try again.&lt;/p&gt;

&lt;p&gt;The general "flow" looks like this&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iap-concept.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zp9gIblF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iap-concept.PNG" alt="Connection flow when using Identity-Aware Proxy (IAP)" width="754" height="774"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using IAP might look overly complicated at first, but it offers &lt;strong&gt;a number of benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we can leverage &lt;strong&gt;Google's authentication system&lt;/strong&gt; and the powerful
IAM permission management to manage permissions - no more need 
to deploy custom SSH keys to the VMs&lt;/li&gt;
&lt;li&gt;we &lt;strong&gt;don't need a public IP address&lt;/strong&gt; any longer, see 
&lt;a href="https://cloud.google.com/iap/docs/tcp-forwarding-overview#how-tcp-forwarding-works"&gt;Overview of TCP forwarding &amp;gt; How IAP's TCP forwarding works&lt;/a&gt;
&amp;gt; A special case, establishing an SSH connection using &lt;code&gt;gcloud compute ssh&lt;/code&gt; wraps the SSH 
&amp;gt; connection inside HTTPS and forwards it to the remote instance without the need of a 
&amp;gt; listening port on local host.
&amp;gt; 
&amp;gt; [...]
&amp;gt; 
&amp;gt; TCP forwarding with IAP doesn't require a public, routable IP address assigned to your 
&amp;gt; resource. Instead, it uses internal IPs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is nice, because in our final PHP application only the &lt;code&gt;nginx&lt;/code&gt; container should be &lt;br&gt;
  accessible publicly - &lt;code&gt;php-fpm&lt;/code&gt; and the &lt;code&gt;php-workers&lt;/code&gt; shouldn't. Usually we would use a&lt;br&gt;&lt;br&gt;
  so-called &lt;a href="https://en.wikipedia.org/wiki/Bastion_host"&gt;Bastion Host or Jump Box&lt;/a&gt; to deal with &lt;br&gt;
  this problem, but thanks to IAP we don't have to&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gochronicles.com/secure-tunnel-with-iap-gcp/"&gt;Accessing Secure Servers Using IAP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/google-cloud/connecting-securely-to-google-compute-engine-vms-without-a-public-ip-or-vpn-720e53d1978e"&gt;Connecting Securely to Google Compute Engine VMs without a Public IP or VPN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;a id="get-root-permissions"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  Get &lt;code&gt;root&lt;/code&gt; permissions
&lt;/h4&gt;

&lt;p&gt;The group  &lt;a href="https://superuser.com/a/1400281/434918"&gt;&lt;code&gt;google-sudoers&lt;/code&gt; exists on each GCP Compute Instance&lt;/a&gt;.  It is configured for &lt;strong&gt;passwordless sudo&lt;/strong&gt; via &lt;code&gt;/etc/sudoers.d/google_sudoers&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo cat /etc/sudoers.d/google_sudoers 
%google-sudoers ALL=(ALL:ALL) NOPASSWD:ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I.e. each user added to this group can use &lt;code&gt;sudo&lt;/code&gt; without password or simply run &lt;code&gt;sudo -i&lt;/code&gt; to  become &lt;code&gt;root&lt;/code&gt;. &lt;strong&gt;Your user should be added automatically to that group&lt;/strong&gt;. This can be verified with  the &lt;code&gt;id&lt;/code&gt; command (run on the VM)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pascal@dofroscra-test:~$ id
uid=1002(Pascal) gid=1003(Pascal) groups=1003(Pascal),4(adm),30(dip),44(video),46(plugdev),1000(google-sudoers)

# =&amp;gt; 1000(google-sudoers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="ssh-and-scp-commands"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; commands
&lt;/h3&gt;

&lt;p&gt;It is quite common &lt;strong&gt;to copy files&lt;/strong&gt; from / to a VM and to &lt;strong&gt;run commands&lt;/strong&gt; on it. This is  usually done via &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt;. The &lt;code&gt;gcloud&lt;/code&gt; cli offers equivalent commands that can also make  use of the Identity-Aware Proxy (IAP) concept:&lt;/p&gt;

&lt;p&gt;&lt;a id="gcloud-ssh-command"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;gcloud ssh --command=""&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Documentation: &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--command"&gt;&lt;code&gt;gcloud ssh --command&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"whoami"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"whoami"&lt;/span&gt;
Pascal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="gcloud-scp"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;gcloud scp&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Documentation: &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/scp"&gt;&lt;code&gt;gcloud scp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute scp &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p ./local-file1.txt ./local-file2.txt dofroscra-test:tmp/

&lt;span class="c"&gt;# ---&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ./local-file1.txt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"2"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ./local-file2.txt
gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rm -rf ~/test; mkdir -p ~/test"&lt;/span&gt;
gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -l ~/test"&lt;/span&gt;
gcloud compute scp &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p ./local-file1.txt ./local-file2.txt dofroscra-test:test/
gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -l ~/test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ./local-file1.txt
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"2"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ./local-file2.txt
&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -l ~/test"&lt;/span&gt;
total 0

&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud compute scp &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p ./local-file1.txt ./local-file2.txt dofroscra-test:test/
local-file1.txt           | 0 kB |   0.0 kB/s | ETA: 00:00:00 | 100%
local-file2.txt           | 0 kB |   0.0 kB/s | ETA: 00:00:00 | 100%

&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -l ~/test"&lt;/span&gt;
total 8
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 Pascal Pascal 4 Jun  2 13:06 local-file1.txt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 Pascal Pascal 4 Jun  2 13:06 local-file2.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: According to the examples in the documentation, it should also be possible to use the  &lt;code&gt;~&lt;/code&gt; to define that the destination on the remote VM is relative to the home directory. However,  this did not work for me, as I kept getting the error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt dofroscra-test:~/test
pscp: remote filespec ~/test: not a directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though the diretory existed. But removing the &lt;code&gt;~/&lt;/code&gt; part worked&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt  dofroscra-test:test/
local-file1.txt           | 0 kB |   0.0 kB/s | ETA: 00:00:00 | 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="provision-the-vm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Provision the VM
&lt;/h2&gt;

&lt;p&gt;&lt;a id="get-the-secret-gpg-key-and-password-from-the-secret-manager"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Get the secret &lt;code&gt;gpg&lt;/code&gt; key and password from the Secret Manager
&lt;/h3&gt;

&lt;p&gt;Before we shift our attention to docker, let's quickly deal with the secrets. We won't need them  for this part of the tutorial. but will need the secret &lt;code&gt;gpg&lt;/code&gt; key and its password to  &lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/#the-secrets-directory"&gt;decrypt the secrets in the &lt;code&gt;php&lt;/code&gt; containers in the next part&lt;/a&gt;. To recap: We have added the them previously and can now retrieve them as explained under section Retrieve a secret via the &lt;code&gt;gcloud&lt;/code&gt; cli via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets versions access 1 &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GPG_KEY &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; secret.gpg

&lt;span class="nv"&gt;GPG_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud secrets versions access 1 &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GPG_PASSWORD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets versions access 1 --secret=GPG_KEY &amp;gt; secret.gpg
$ head secret.gpg
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQPGBGKA1psBCACq5zYDT587CVZEIWXbUplfAGQZOQJALmzErYpTp0jt+rp4vJhR
U5xahy3pqCq81Cnny5YME50ybB3pW/WcHxWLBDo+he8PKeLbp6wFFjJns+3u4opH
9gFMElyHpzTGiDQYfx/CgY2hKz7GSqpjmnOaKxYvGv0EsbZczyHY1WIN/YFzb0tI
tY7J4zTSH05I+aazRdHyn28QcCRcIT9+4q+5Vk8gz8mmgoqVpyeNgQcqJjcd03iP
WUZd1vZCumOvdG5PZNlc/wPFhqLDmYyLmJ7pt5bWIgty9BjYK8Z2NOdUaekqVEJ+
r29HbzwgFLLE2gd52f07h2y2YgMdWdz4FDxVABEBAAH+BwMC9veBYT2oigXxExLl
7fZKVjw02lEr1NpYd5X1ge9WPU/1qumATJWounzciiETpsYGsbPd9zFRJP4E3JZl
sFSh4p0/kXYTuenYD8wgGkeYyN4lm53IHfqSn2z9JMW5Kz9XEODtKJl8fjcn9Zeb

$ GPG_PASSWORD=$(gcloud secrets versions access 1 --secret=GPG_PASSWORD)
$ echo $GPG_PASSWORD
87654321
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="installing-docker-and-docker-compose"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;docker compose&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Since we created the VM with a Debian OS, we'll follow the  &lt;a href="https://docs.docker.com/engine/install/debian/"&gt;official Debian installation instructions for Docker Engine&lt;/a&gt;  and run the following commands while we are logged into the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install required tools&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     ca-certificates &lt;span class="se"&gt;\&lt;/span&gt;
     curl &lt;span class="se"&gt;\&lt;/span&gt;
     gnupg &lt;span class="se"&gt;\&lt;/span&gt;
     lsb-release

&lt;span class="c"&gt;# add Docker’s official GPG key&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/debian/gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/docker-archive-keyring.gpg

&lt;span class="c"&gt;# set up the stable repository&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
 &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-cs&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# install Docker Engine&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     docker-ce &lt;span class="se"&gt;\&lt;/span&gt;
     docker-ce-cli &lt;span class="se"&gt;\&lt;/span&gt;
     containerd.io &lt;span class="se"&gt;\&lt;/span&gt;
     docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have also added these instructions to the script &lt;code&gt;.infrastructure/scripts/provision.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Afterwards we check via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;if &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;docker compose&lt;/code&gt; are available&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker --version
Docker version 20.10.15, build fd82621

$ docker compose version
Docker Compose version v2.5.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="authenticate-docker-via-gcloud"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authenticate docker via &lt;code&gt;gcloud&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I recommend running the commands as &lt;code&gt;root&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pascal_landau@dofroscra-test:~$ sudo -i
root@dofroscra-test:~# 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, we might run into docker permission errors like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/images/create?fromImage=...": dial unix /var/run/docker.sock: connect: permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI: As an alternative we could also add the non-root user to the &lt;code&gt;docker&lt;/code&gt; group via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; docker &lt;span class="nv"&gt;$user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ user=$(whoami)
$ sudo usermod -a -G docker $user
$ id
uid=1001(pascal_landau) gid=1002(pascal_landau) groups=1002(pascal_landau),4(adm),30(dip),44(video),46(plugdev),997(docker),1000(google-sudoers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locally, we could  use the &lt;code&gt;JSON&lt;/code&gt; key file of the service account for authentication, but we  don't have access to that key file on the VM. Instead, we will authenticate &lt;code&gt;docker&lt;/code&gt; via the  pre-installed &lt;code&gt;gcloud&lt;/code&gt; cli and the attached service account as described in the  &lt;a href="https://cloud.google.com/container-registry/docs/advanced-authentication#gcloud-helper"&gt;GCP docs for the Container Registry Authentication methods under section "gcloud credential helper"&lt;/a&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth configure-docker &lt;span class="nt"&gt;--quiet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
Docker configuration file updated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the file &lt;code&gt;/root/.docker/config.json&lt;/code&gt; that we already encountered when we authenticated docker locally to push images. In this case it has the following content&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat /root/.docker/config.json
{
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.docker.com/engine/reference/commandline/login/#credential-helpers"&gt;&lt;code&gt;creadHelpers&lt;/code&gt; are described in the &lt;code&gt;docker login&lt;/code&gt; docs&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Credential helpers are similar to the credential store above, but act as the &lt;strong&gt;designated &lt;br&gt;
programs to handle credentials&lt;/strong&gt; for specific registries. The &lt;strong&gt;default credential store&lt;/strong&gt;&lt;br&gt;
(credsStore or the config file itself) &lt;strong&gt;will not be used&lt;/strong&gt; for operations concerning &lt;br&gt;
credentials of the specified registries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we are using &lt;code&gt;gcr.io&lt;/code&gt; as registry&lt;/li&gt;
&lt;li&gt;this registry is defined to use the credential helper &lt;code&gt;gcloud&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;i.e. it will use the pre-initialized &lt;code&gt;gcloud&lt;/code&gt; cli for authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="pulling-the-nginx-image"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pulling the &lt;code&gt;nginx&lt;/code&gt; image
&lt;/h3&gt;

&lt;p&gt;Once we are authenticated, we can simply run &lt;code&gt;docker pull&lt;/code&gt; with the full image name to retrieve the  &lt;code&gt;nginx&lt;/code&gt; image that we pushed previously&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull gcr.io/pl-dofroscra-p/my-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
59bf1c3509f3: Pull complete 
f3322597df46: Pull complete 
d09cf91cabdc: Pull complete 
3a97535ac2ef: Pull complete 
919ade35f869: Pull complete 
40e5d2fe5bcd: Pull complete 
c72acb0c83a5: Pull complete 
d6baa2bee4a5: Pull complete 
Digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f
Status: Downloaded newer image for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we wouldn't be authenticated, we would run into the following error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
Error response from daemon: unauthorized: You don't have the needed permissions to perform this operation, and you may have invalid credentials. To authenticate your request, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="start-the-nginx-container"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Start the &lt;code&gt;nginx&lt;/code&gt; container
&lt;/h3&gt;

&lt;p&gt;For now, we will simply run the &lt;code&gt;nginx&lt;/code&gt; container with &lt;code&gt;docker run&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; nginx &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; gcr.io/pl-dofroscra-p/my-nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;give it the name &lt;code&gt;nginx&lt;/code&gt;&lt;/strong&gt; so we can easily reference it later via &lt;code&gt;--name nginx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;map port &lt;code&gt;80&lt;/code&gt; from the host to port &lt;code&gt;80&lt;/code&gt; of the container&lt;/strong&gt; so that HTTP requests to the 
VM are handled via the container via &lt;code&gt;-p 80:80&lt;/code&gt;, 
see &lt;a href="https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose"&gt;Docker docs on "Publish or expose port (-p, --expose)"&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;make it &lt;strong&gt;run in the background&lt;/strong&gt; via &lt;code&gt;-d&lt;/code&gt; (&lt;code&gt;--detach&lt;/code&gt;) and &lt;strong&gt;remove it after shutdown&lt;/strong&gt;
automatically via &lt;code&gt;-rm&lt;/code&gt; (&lt;code&gt;--remove&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@dofroscra-test:~# docker run -p 80:80 -d --name nginx gcr.io/pl-dofroscra-p/my-nginx:latest
dd49bedad97c06f698d06a140c5091c04ad81b2f75632e222927e7f71cf28c18

root@dofroscra-test:~# docker logs nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/05/08 12:01:43 [notice] 1#1: using the "epoll" event method
2022/05/08 12:01:43 [notice] 1#1: nginx/1.21.5
2022/05/08 12:01:43 [notice] 1#1: built by gcc 10.3.1 20211027 (Alpine 10.3.1_git20211027) 
2022/05/08 12:01:43 [notice] 1#1: OS: Linux 4.19.0-20-cloud-amd64
2022/05/08 12:01:43 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2022/05/08 12:01:43 [notice] 1#1: start worker processes
2022/05/08 12:01:43 [notice] 1#1: start worker process 30
2022/05/08 12:01:43 [notice] 1#1: start worker process 31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're &lt;strong&gt;all set to serve HTTP requests&lt;/strong&gt;: The requests will be passed to the &lt;code&gt;nginx&lt;/code&gt; container  due to the mapped port &lt;code&gt;80&lt;/code&gt;, i.e. we can simply use the public IP address &lt;code&gt;35.192.212.130&lt;/code&gt; in a  &lt;code&gt;curl&lt;/code&gt; request and check  the "Hello world" file we've added to the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s http://35.192.212.130/hello.html
Hello world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="automate-via-gcloud-commands"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate via &lt;code&gt;gcloud&lt;/code&gt; commands
&lt;/h2&gt;

&lt;p&gt;So far we &lt;strong&gt;have mostly used the UI to configure everything&lt;/strong&gt;. This is great for understanding  how  things work, but it's not so great for maintainability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UIs change&lt;/li&gt;
&lt;li&gt;"Clicking" stuff takes quite some time&lt;/li&gt;
&lt;li&gt;we can't automate anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, we can also &lt;strong&gt;use the &lt;code&gt;gcloud&lt;/code&gt; cli for most of the  tasks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="preconditions-project-and-owner-service-account"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Preconditions: Project and &lt;code&gt;Owner&lt;/code&gt; service account
&lt;/h3&gt;

&lt;p&gt;I'll assume that &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gcloud&lt;/code&gt; is installed&lt;/li&gt;
&lt;li&gt;a GCP project exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition, I'll manually create a new "master" service account  and assign it &lt;a href="https://cloud.google.com/iam/docs/understanding-roles#basic-definitions"&gt;the role &lt;code&gt;Owner&lt;/code&gt;&lt;/a&gt;.  We'll use that service account in the &lt;code&gt;gcloud&lt;/code&gt; cli to enable all the APIs and manage the  resources. &lt;/p&gt;

&lt;p&gt;FYI: We could also do this with our personal GCP user, but I plan to use &lt;code&gt;terraform&lt;/code&gt; in a later  tutorial which will require a service account anyway.&lt;/p&gt;

&lt;p&gt;We will use &lt;code&gt;docker-php-tutorial-master&lt;/code&gt; as service account ID and  store the key file of the account at the root of the  codebase under &lt;code&gt;gcp-master-service-account-key.json&lt;/code&gt; (which is also added to the &lt;code&gt;.gitignore&lt;/code&gt; file).&lt;/p&gt;

&lt;p&gt;&lt;a id="configure-gcloud-to-use-the-master-service-account"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure &lt;code&gt;gcloud&lt;/code&gt; to use the master service account
&lt;/h3&gt;

&lt;p&gt;Run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth activate-service-account &lt;span class="nt"&gt;--key-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gcp-master-service-account-key.json &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud auth activate-service-account --key-file=./gcp-master-service-account-key.json --project=pl-dofroscra-p
Activated service account credentials for: [docker-php-tutorial-master@pl-dofroscra-p.iam.gserviceaccount.com]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="enable-apis"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable APIs
&lt;/h3&gt;

&lt;p&gt;APIs can be enabled via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/services/enable"&gt;&lt;code&gt;gcloud services enable $serviceName&lt;/code&gt;&lt;/a&gt;, (see also the &lt;a href="https://cloud.google.com/endpoints/docs/openapi/enable-api"&gt;Docu on "Enabling an API in your Google Cloud project"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$serviceName&lt;/code&gt; is shown on the API overview page of a service, see the following example for  the &lt;a href="https://console.cloud.google.com/marketplace/product/google/containerregistry.googleapis.com"&gt;Container Registry&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-api-service-name.PNG"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vnaf7EBA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-api-service-name.PNG" alt="Find the service name of an API" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We need the APIs for&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container Registry: &lt;code&gt;containerregistry.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Secret Manager: &lt;code&gt;secretmanager.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute Engine: &lt;code&gt;compute.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;IAM: &lt;code&gt;iam.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cloud Storage: &lt;code&gt;storage.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cloud Resource Manager: &lt;code&gt;cloudresourcemanager.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;containerregistry.googleapis.com secretmanager.googleapis.com compute.googleapis.com iam.googleapis.com storage.googleapis.com cloudresourcemanager.googleapis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud services enable containerregistry.googleapis.com secretmanager.googleapis.com compute.googleapis.com iam.googleapis.com storage.googleapis.com cloudresourcemanager.googleapis.com
Operation "operations/acat.p2-386551299607-87333b29-c7eb-40b8-b951-8c86185bbf49" finished successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="create-and-configure-a-deployment-service-account"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create and configure a "deployment" service account
&lt;/h3&gt;

&lt;p&gt;We won't use the master &lt;code&gt;Owner&lt;/code&gt; service account for the deployment but create a custom one with  only the necessary permissions.&lt;/p&gt;

&lt;p&gt;Service accounts are created via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/create"&gt;&lt;code&gt;gcloud iam service-accounts create $serviceAccountId&lt;/code&gt;&lt;/a&gt;, (see also the &lt;a href="https://cloud.google.com/iam/docs/creating-managing-service-accounts"&gt;Docu on "Creating and managing service accounts"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$serviceAccountId&lt;/code&gt; is the &lt;strong&gt;id of the service account&lt;/strong&gt;, e.g. &lt;code&gt;docker-php-tutorial-deployment&lt;/code&gt; in our previous example. In  addition, we can define a &lt;code&gt;--description&lt;/code&gt; and a &lt;code&gt;--display-name&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create docker-php-tutorial-deployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Used for the deployment of the Docker PHP Tutorial application"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Docker PHP Tutorial Deployment Account"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud iam service-accounts create docker-php-tutorial-deployment \
&amp;gt;   --description="Used for the deployment of the Docker PHP Tutorial application" \
&amp;gt;   --display-name="Docker PHP Tutorial Deployment Account"
Created service account [docker-php-tutorial-deployment].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we need a &lt;strong&gt;key file&lt;/strong&gt; for authentication. It can be created via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/keys/create"&gt;&lt;code&gt;gcloud iam service-accounts keys create $localPathToKeyFile --iam-account=$serviceAccountEmail&lt;/code&gt;&lt;/a&gt; (see also the &lt;a href="https://cloud.google.com/iam/docs/creating-managing-service-account-keys"&gt;Docu on "Create and manage service account keys"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$localPathToKeyFile&lt;/code&gt; is used to &lt;strong&gt;store the key file locally&lt;/strong&gt;, e.g. &lt;code&gt;gcp-service-account-key.json&lt;/code&gt; in our previous example and  &lt;code&gt;$serviceAccountEmail&lt;/code&gt; is the email address of the service account. It has the form&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$serviceAccountId@$projectId.iam.gserviceaccount.com

e.g.

docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts keys create ./gcp-service-account-key.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud iam service-accounts keys create ./gcp-service-account-key.json \
&amp;gt;   --iam-account=docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
created key [810df0c6df21de44dc5e431d2b569d74555ba3f9] of type [json] as [./gcp-service-account-key.json] for [docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we must assign the required IAM roles via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/add-iam-policy-binding"&gt;&lt;code&gt;gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=$roleId&lt;/code&gt;&lt;/a&gt;, (see also the &lt;a href="https://cloud.google.com/iam/docs/granting-changing-revoking-access#grant-single-role"&gt;Docu on "Manage access to projects, folders, and organizations"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$projectName&lt;/code&gt; is the &lt;strong&gt;name of the GCP project&lt;/strong&gt;, &lt;code&gt;$serviceAccountEmail&lt;/code&gt; the email address  from before and the &lt;code&gt;$roleId&lt;/code&gt; the &lt;strong&gt;name of the role&lt;/strong&gt; as listed under &lt;a href="https://cloud.google.com/iam/docs/understanding-roles"&gt;Understanding roles&lt;/a&gt;. In our case that's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storage Admin: &lt;code&gt;roles/storage.admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Secret Manager Admin: &lt;code&gt;roles/secretmanager.admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compute Admin: &lt;code&gt;roles/compute.admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Service Account User: &lt;code&gt;roles/iam.serviceAccountUser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;IAP-secured Tunnel User: &lt;code&gt;roles/iap.tunnelResourceAccessor&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;projectName="pl-dofroscra-p"
serviceAccountEmail="docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/storage.admin
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/secretmanager.admin
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/compute.admin
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/iam.serviceAccountUser
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/iap.tunnelResourceAccessor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ projectName="pl-dofroscra-p"
$ serviceAccountEmail="docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com"
$ gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/storage.admin
Updated IAM policy for project [pl-dofroscra-p].
bindings:
- members:
  - serviceAccount:service-386551299607@compute-system.iam.gserviceaccount.com
  role: roles/compute.serviceAgent
- members:
  - serviceAccount:service-386551299607@containerregistry.iam.gserviceaccount.com
  role: roles/containerregistry.ServiceAgent
- members:
  - serviceAccount:386551299607-compute@developer.gserviceaccount.com
  - serviceAccount:386551299607@cloudservices.gserviceaccount.com
  role: roles/editor
- members:
  - serviceAccount:docker-php-tutorial-master@pl-dofroscra-p.iam.gserviceaccount.com
  - user:pascal.landau@gmail.com
  role: roles/owner
- members:
  - serviceAccount:service-386551299607@gcp-sa-pubsub.iam.gserviceaccount.com
  role: roles/pubsub.serviceAgent
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/storage.admin
etag: BwXgKxHg7gA=
version: 1

# ...

Updated IAM policy for project [pl-dofroscra-p].
bindings:
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/compute.admin
- members:
  - serviceAccount:service-386551299607@compute-system.iam.gserviceaccount.com
  role: roles/compute.serviceAgent
- members:
  - serviceAccount:service-386551299607@containerregistry.iam.gserviceaccount.com
  role: roles/containerregistry.ServiceAgent
- members:
  - serviceAccount:386551299607-compute@developer.gserviceaccount.com
  - serviceAccount:386551299607@cloudservices.gserviceaccount.com
  role: roles/editor
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/iam.serviceAccountUser
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/iap.tunnelResourceAccessor
- members:
  - serviceAccount:docker-php-tutorial-master@pl-dofroscra-p.iam.gserviceaccount.com
  - user:pascal.landau@gmail.com
  role: roles/owner
- members:
  - serviceAccount:service-386551299607@gcp-sa-pubsub.iam.gserviceaccount.com
  role: roles/pubsub.serviceAgent
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/secretmanager.admin
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/storage.admin
etag: BwXgKxm0Vtk=
version: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="create-secrets"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create secrets
&lt;/h3&gt;

&lt;p&gt;Secrets are created via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/secrets/create"&gt;&lt;code&gt;gcloud secrets create $secretId"&lt;/code&gt;&lt;/a&gt;, (see also the &lt;a href="https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"&gt;Docu on "Creating and accessing secrets"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$secretId&lt;/code&gt; is the &lt;strong&gt;name of the secret&lt;/strong&gt;, e.g.  &lt;code&gt;my_secret_key&lt;/code&gt; in our previous example, and we must also add a  new version with the &lt;strong&gt;value of the secret&lt;/strong&gt; via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/secrets/versions/add"&gt;&lt;code&gt;gcloud secrets versions add $secretId --data-file="/path/to/file.txt"&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets create my_secret_key

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"my_secret_value"&lt;/span&gt; | gcloud secrets versions add my_secret_key &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets create my_secret_key
Created secret [my_secret_key].

$ echo -n "my_secret_value" | gcloud secrets versions add my_secret_key --data-file=-
Created version [1] of the secret [my_secret_key].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to  do the same for the &lt;code&gt;gpg&lt;/code&gt; secret key and its password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets create GPG_KEY
&lt;span class="nb"&gt;echo &lt;/span&gt;gcloud secrets versions add GPG_KEY &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret-production-protected.gpg.example

gcloud secrets create GPG_PASSWORD
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"87654321"&lt;/span&gt; | gcloud secrets versions add GPG_PASSWORD &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets create GPG_KEY
Created secret [GPG_KEY].
$ gcloud secrets versions add GPG_KEY --data-file=secret-production-protected.gpg.example
Created version [1] of the secret [GPG_KEY].

$ gcloud secrets create GPG_PASSWORD
Created secret [GPG_PASSWORD].
$ echo -n "87654321" | gcloud secrets versions add GPG_PASSWORD --data-file=-
Created version [1] of the secret [GPG_PASSWORD].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="create-firewall-rule-for-http-traffic"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create firewall rule for HTTP traffic
&lt;/h3&gt;

&lt;p&gt;Firewall rules can be created via &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/firewall-rules/create"&gt;&lt;code&gt;gcloud compute firewall-rules create $ruleName&lt;/code&gt;&lt;/a&gt; (see also the &lt;a href="https://cloud.google.com/vpc/docs/using-firewalls#creating_firewall_rules"&gt;Docu on "Using firewall rules"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;We'll stick to  the same conventions that have been used when creating the VM via the UI by using &lt;code&gt;default-allow-http&lt;/code&gt; as the &lt;code&gt;$ruleName&lt;/code&gt; and &lt;code&gt;http-server&lt;/code&gt; as the network tag (via the  &lt;code&gt;--target-tags&lt;/code&gt; option)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; gcloud compute firewall-rules create default-allow-http &lt;span class="nt"&gt;--allow&lt;/span&gt; tcp:80 &lt;span class="nt"&gt;--target-tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute firewall-rules create default-allow-http --allow tcp:80 --target-tags=http-server
Creating firewall...
..Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/global/firewalls/default-allow-http].
done.
NAME                NETWORK  DIRECTION  PRIORITY  ALLOW   DENY  DISABLED
default-allow-http  default  INGRESS    1000      tcp:80        False

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="create-a-compute-instance-vm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Compute Instance VM
&lt;/h3&gt;

&lt;p&gt;Compute Instances can be created via  &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/create"&gt;&lt;code&gt;gcloud compute instances create $vmName&lt;/code&gt;&lt;/a&gt; (see also the &lt;a href="https://cloud.google.com/compute/docs/instances/create-start-instance#publicimage"&gt;Docu on "Creating and starting a VM instance"&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$vmName&lt;/code&gt; defines the &lt;strong&gt;name of the VM&lt;/strong&gt;, e.g. &lt;code&gt;dofroscra-test&lt;/code&gt; in the previous example. In addition, there are a lot of  options to customize the VM, e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--image-family&lt;/code&gt; and &lt;code&gt;--image-project&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;define the &lt;strong&gt;operating system&lt;/strong&gt;, e.g. &lt;code&gt;--image-family="debian-11"&lt;/code&gt; and 
&lt;code&gt;--image-project=debian-cloud&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;See &lt;a href="https://cloud.google.com/compute/docs/images/os-details"&gt;Docu on "Operating system details"&lt;/a&gt;
for a list of available values&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--machine-type&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;defines &lt;strong&gt;the machine type&lt;/strong&gt; (i.e. the "specs" like CPUs and memory), e.g.
&lt;code&gt;--machine-type=e2-micro&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;See &lt;a href="https://cloud.google.com/compute/docs/machine-types"&gt;Docu on "About machine families"&lt;/a&gt; 
that contains links to the machine type categories with the concrete values, e.g. the
&lt;a href="https://cloud.google.com/compute/docs/general-purpose-machines"&gt;General-purpose machine family&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/create"&gt;docu&lt;/a&gt; is doing  a great job at describing all available configuration options. Luckily, we can make our lives a  little easier, &lt;strong&gt;configure the VM via UI&lt;/strong&gt; and then click the &lt;code&gt;"EQUIVALENT COMMAND LINE"&lt;/code&gt; button  at the end of the page to get a &lt;strong&gt;copy-paste-ready &lt;code&gt;gcloud compute instances create&lt;/code&gt; command&lt;/strong&gt;.&lt;/p&gt;


  
Your browser does not support the video tag.


&lt;p&gt;Note that we are using &lt;code&gt;dofroscra-test&lt;/code&gt; as the &lt;strong&gt;instance name&lt;/strong&gt; and  &lt;code&gt;us-central1-a&lt;/code&gt; as the&lt;br&gt;
&lt;strong&gt;zone&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instances create dofroscra-test &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1-a &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e2-micro &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network-interface&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-tier&lt;span class="o"&gt;=&lt;/span&gt;PREMIUM,subnet&lt;span class="o"&gt;=&lt;/span&gt;default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-restart-on-failure&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--maintenance-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERMINATE &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SPOT &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instance-termination-action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;STOP &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.googleapis.com/auth/cloud-platform &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http-server &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--create-disk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto-delete&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;,boot&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;,device-name&lt;span class="o"&gt;=&lt;/span&gt;dofroscra-test,image&lt;span class="o"&gt;=&lt;/span&gt;projects/debian-cloud/global/images/debian-11-bullseye-v20220519,mode&lt;span class="o"&gt;=&lt;/span&gt;rw,size&lt;span class="o"&gt;=&lt;/span&gt;10,type&lt;span class="o"&gt;=&lt;/span&gt;projects/pl-dofroscra-p/zones/us-central1-a/diskTypes/pd-balanced &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-shielded-secure-boot&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--shielded-vtpm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--shielded-integrity-monitoring&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--reservation-affinity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;any
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/zones/us-central1-a/instances/dofroscra-test-1].

NAME            ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
dofroscra-test  us-central1-a  e2-small      true         10.128.0.2   34.122.227.169  RUNNING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="provisioning"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Provisioning
&lt;/h3&gt;

&lt;p&gt;For provisioning, we need to install &lt;code&gt;docker&lt;/code&gt; and  authenticate the &lt;code&gt;root&lt;/code&gt; user to pull images from our registry. We've already created an installation script at &lt;code&gt;.infrastructure/scripts/provision.sh&lt;/code&gt; and the  easiest way to run it on the VM is to transmit it via &lt;code&gt;scp&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute scp &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p ./.infrastructure/scripts/provision.sh dofroscra-test:provision.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./.infrastructure/scripts/provision.sh dofroscra-test:provision.sh
provision.sh              | 0 kB |   0.7 kB/s | ETA: 00:00:00 | 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The previous command transmitted the script in the home directory of the user, and we can now  execute it via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bash provision.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="bash provision.sh"
Hit:1 https://download.docker.com/linux/debian bullseye InRelease
Hit:2 http://packages.cloud.google.com/apt cloud-sdk-bullseye InRelease
# ...
The following additional packages will be installed:
  dbus-user-session docker-ce-rootless-extras docker-scan-plugin git git-man
  libcurl3-gnutls liberror-perl libgdbm-compat4 libltdl7 libperl5.32 libslirp0
  patch perl perl-modules-5.32 pigz slirp4netns
Suggested packages:
  aufs-tools cgroupfs-mount | cgroup-lite git-daemon-run | git-daemon-sysvinit
  git-doc git-el git-email git-gui gitk gitweb git-cvs git-mediawiki git-svn
  ed diffutils-doc perl-doc libterm-readline-gnu-perl
  | libterm-readline-perl-perl make libtap-harness-archive-perl
The following NEW packages will be installed:
  containerd.io dbus-user-session docker-ce docker-ce-cli
  docker-ce-rootless-extras docker-compose-plugin docker-scan-plugin git
  git-man libcurl3-gnutls liberror-perl libgdbm-compat4 libltdl7 libperl5.32
  libslirp0 patch perl perl-modules-5.32 pigz slirp4netns
0 upgraded, 20 newly installed, 0 to remove and 6 not upgraded.
Need to get 124 MB of archives.
After this operation, 535 MB of additional disk space will be used.
# ...
Setting up docker-ce (5:20.10.16~3-0~debian-bullseye) ...
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /lib/systemd/system/docker.service.
Created symlink /etc/systemd/system/sockets.target.wants/docker.socket → /lib/systemd/system/docker.socket.
Setting up liberror-perl (0.17029-1) ...
Setting up git (1:2.30.2-1) ...
Processing triggers for man-db (2.9.4-2) ...
Processing triggers for libc-bin (2.31-13+deb11u3) ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once &lt;code&gt;docker&lt;/code&gt; is installed, we can run the authentication of the &lt;code&gt;root&lt;/code&gt; user using  &lt;a href="https://unix.stackexchange.com/a/87861"&gt;&lt;code&gt;sudo su root -c&lt;/code&gt;&lt;/a&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sudo su root -c 'gcloud auth configure-docker --quiet'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="sudo su root -c 'gcloud auth configure-docker --quiet'"
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
gcloud credential helpers already registered correctly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="deployment"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;p&gt;We're almost done - the last step in the process consists of &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;building the docker image locally using the correct tag &lt;code&gt;gcr.io/pl-dofroscra-p/my-nginx&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"gcr.io/pl-dofroscra-p/my-nginx"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
  FROM nginx:1.21.5-alpine

  RUN echo "Hello world" &amp;gt;&amp;gt; /usr/share/nginx/html/hello.html
&lt;/span&gt;&lt;span class="no"&gt;
  EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ docker build -t "gcr.io/pl-dofroscra-p/my-nginx" --no-cache -f - . &amp;lt;&amp;lt;EOF
  FROM nginx:1.21.5-alpine
  RUN echo "Hello world" &amp;gt;&amp;gt; /usr/share/nginx/html/hello.html
  EOF

  #1 [internal] load build definition from Dockerfile
  #1 sha256:21ee68236cc00e4d1638480de32fd6304d796e6a72306082d3164e9164073843

  #...

  #5 [2/2] RUN echo "Hello world" &amp;gt;&amp;gt; /usr/share/nginx/html/hello.html
  #5 sha256:72dee3f3637a6b78ff7e50591e3e9108e2b93eee09443e19890f9cb36b35bdc6
  #5 DONE 0.5s

  #6 exporting to image
  #6 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
  #6 exporting layers 0.1s done
  #6 writing image sha256:d0b23a80ebd7311953eeff3818b58007e8747e531e0128eef820f39105f9f1fe done
  #6 naming to gcr.io/pl-dofroscra-p/my-nginx done
  #6 DONE 0.1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;local authentication
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;cat&lt;/span&gt; ./gcp-service-account-key.json | docker login &lt;span class="nt"&gt;-u&lt;/span&gt; _json_key &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; https://gcr.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ cat ./gcp-service-account-key.json | docker login -u _json_key --password-stdin https://gcr.io
  Login Succeeded

  Logging in with your password grants your terminal complete access to your account.
  For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
pushing it to the registry
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker push gcr.io/pl-dofroscra-p/my-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
  $ docker push gcr.io/pl-dofroscra-p/my-nginx
  Using default tag: latest
  The push refers to repository [gcr.io/pl-dofroscra-1/my-nginx]
  ad4683501621: Preparing
  # ...
  ad4683501621: Pushed
  latest: digest: sha256:680086b3c77e4b895099c0a5f6e713ff79ba0d78e1e1df1bc2546d6f979126e4 size: 1775
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;and "deploying it on the VM"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the last step we will once again use a script that we transmit to the VM: &lt;code&gt;.infrastructure/scripts/deploy.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Usage: deploy.sh image_name"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No image_name given! &lt;/span&gt;&lt;span class="nv"&gt;$usage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;container_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Pulling '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' from registry"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker pull &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting container"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;container_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;container_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Getting secret GPG_KEY"&lt;/span&gt;
gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GPG_KEY &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ./secret.gpg
&lt;span class="nb"&gt;head&lt;/span&gt; ./secret.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Getting secret GPG_PASSWORD"&lt;/span&gt;
&lt;span class="nv"&gt;GPG_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GPG_PASSWORD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$GPG_PASSWORD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script will pull the image &lt;em&gt;on the VM&lt;/em&gt; from the registry&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo docker pull 'gcr.io/pl-dofroscra-p/my-nginx'
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
# ...
Digest: sha256:680086b3c77e4b895099c0a5f6e713ff79ba0d78e1e1df1bc2546d6f979126e4
Status: Downloaded newer image for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;start a container with the image&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker kill "${container_name}"; sudo docker run --name "${container_name}" -p 80:80 --rm -d "${image_name}"
Error response from daemon: Cannot kill container: nginx: No such container: nginx
8ac0cc055041e18d3ce244ded49c82be985e580c2c9f316c6d6ef7c7bf3bc0b5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and finally retrieve the secrets  (just to demonstrate that it works)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gcloud secrets versions access latest --secret=GPG_KEY &amp;gt;&amp;gt; ./secret.gpg
$ head ./secret.gpg
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQPGBGKA1psBCACq5zYDT587CVZEIWXbUplfAGQZOQJALmzErYpTp0jt+rp4vJhR
U5xahy3pqCq81Cnny5YME50ybB3pW/WcHxWLBDo+he8PKeLbp6wFFjJns+3u4opH
9gFMElyHpzTGiDQYfx/CgY2hKz7GSqpjmnOaKxYvGv0EsbZczyHY1WIN/YFzb0tI
tY7J4zTSH05I+aazRdHyn28QcCRcIT9+4q+5Vk8gz8mmgoqVpyeNgQcqJjcd03iP
WUZd1vZCumOvdG5PZNlc/wPFhqLDmYyLmJ7pt5bWIgty9BjYK8Z2NOdUaekqVEJ+
r29HbzwgFLLE2gd52f07h2y2YgMdWdz4FDxVABEBAAH+BwMC9veBYT2oigXxExLl
7fZKVjw02lEr1NpYd5X1ge9WPU/1qumATJWounzciiETpsYGsbPd9zFRJP4E3JZl
sFSh4p0/kXYTuenYD8wgGkeYyN4lm53IHfqSn2z9JMW5Kz9XEODtKJl8fjcn9Zeb

$ GPG_PASSWORD=$(gcloud secrets versions access latest --secret=GPG_PASSWORD)
$ echo $GPG_PASSWORD
87654321
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bonus: For  &lt;a href="https://cloud.google.com/compute/docs/instances/view-ip-address"&gt;retrieving the puplic IP address of the Compute Instance VM&lt;/a&gt;  we can use the &lt;a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/describe"&gt;&lt;code&gt;gcloud compute instances describe $instanceName&lt;/code&gt;&lt;/a&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instances describe dofroscra-test &lt;span class="nt"&gt;--zone&lt;/span&gt; us-central1-a &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pl-dofroscra-p &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'get(networkInterfaces[0].accessConfigs[0].natIP)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ip=$(gcloud compute instances describe dofroscra-test --zone us-central1-a --project=pl-dofroscra-p --format='get(networkInterfaces[0].accessConfigs[0].natIP)')
$ echo $ip
35.224.250.208
$ curl -s "http://${ip}/hello.html"
Hello world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="putting-it-all-together"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Since none of the previous steps requires manual intervention any longer, I have created a  script at &lt;code&gt;.infrastructure/setup-gcp.sh&lt;/code&gt; to run everything from Configure &lt;code&gt;gcloud&lt;/code&gt; to use the master service account to Provisioning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Usage: deploy.sh project_id vm_name"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No project_id given! &lt;/span&gt;&lt;span class="nv"&gt;$usage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No vm_name given! &lt;/span&gt;&lt;span class="nv"&gt;$usage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0;32m"&lt;/span&gt;
&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0m"&lt;/span&gt;

&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1-a
&lt;span class="nv"&gt;master_service_account_key_location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gcp-master-service-account-key.json
&lt;span class="nv"&gt;deployment_service_account_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deployment
&lt;span class="nv"&gt;deployment_service_account_key_location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gcp-service-account-key.json
&lt;span class="nv"&gt;deployment_service_account_mail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iam.gserviceaccount.com"&lt;/span&gt;
&lt;span class="nv"&gt;gpg_secret_key_location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret-production-protected.gpg.example
&lt;span class="nv"&gt;gpg_secret_key_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;87654321

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Setting up GCP project for&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==="&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"project_id: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"vm_name:    &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Activating master service account&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud auth activate-service-account &lt;span class="nt"&gt;--key-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;master_service_account_key_location&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Enabling APIs&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud services &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  containerregistry.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  secretmanager.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  compute.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  iam.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  storage.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  cloudresourcemanager.googleapis.com

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Creating deployment service account with id '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud iam service-accounts create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Used for the deployment application"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Deployment Account"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Creating JSON key file for deployment service account at &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_key_location&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud iam service-accounts keys create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_key_location&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_mail&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Adding roles for service account&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
&lt;span class="nv"&gt;roles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"storage.admin secretmanager.admin compute.admin iam.serviceAccountUser iap.tunnelResourceAccessor"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;role &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$roles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;gcloud projects add-iam-policy-binding &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;serviceAccount:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_mail&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"--role=roles/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Creating secrets&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud secrets create GPG_KEY
&lt;span class="nb"&gt;echo &lt;/span&gt;gcloud secrets versions add GPG_KEY &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;gpg_secret_key_location&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

gcloud secrets create GPG_PASSWORD
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;gpg_secret_key_password&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | gcloud secrets versions add GPG_PASSWORD &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Creating firewall rule to allow HTTP traffic&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud compute firewall-rules create default-allow-http &lt;span class="nt"&gt;--allow&lt;/span&gt; tcp:80 &lt;span class="nt"&gt;--target-tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http-server

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Creating a Compute Instance VM&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud compute instances create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e2-micro &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network-interface&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-tier&lt;span class="o"&gt;=&lt;/span&gt;PREMIUM,subnet&lt;span class="o"&gt;=&lt;/span&gt;default &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-restart-on-failure&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--maintenance-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERMINATE &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SPOT &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instance-termination-action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;STOP &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_mail&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.googleapis.com/auth/cloud-platform &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http-server &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--create-disk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto-delete&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;,boot&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;,device-name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;,image&lt;span class="o"&gt;=&lt;/span&gt;projects/debian-cloud/global/images/debian-11-bullseye-v20220519,mode&lt;span class="o"&gt;=&lt;/span&gt;rw,size&lt;span class="o"&gt;=&lt;/span&gt;10,type&lt;span class="o"&gt;=&lt;/span&gt;projects/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/zones/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/diskTypes/pd-balanced &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-shielded-secure-boot&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--shielded-vtpm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--shielded-integrity-monitoring&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--reservation-affinity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;any

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Activating deployment service account&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud auth activate-service-account &lt;span class="nt"&gt;--key-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_key_location&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Transferring provisioning script&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting 60s for the instance to be fully ready to receive IAP connections"&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;60
gcloud compute scp &lt;span class="nt"&gt;--zone&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; ./.infrastructure/scripts/provision.sh &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:provision.sh

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Executing provisioning script&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud compute ssh &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bash provision.sh"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Authenticating docker via gcloud in the VM&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud compute ssh &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sudo su root -c 'gcloud auth configure-docker --quiet'"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Provisioning done!&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bash .infrastructure/setup-gcp.sh pl-dofroscra-p dofroscra-test
Setting up GCP project for
===
project_id: pl-dofroscra-p
vm_name:    dofroscra-test
Activating master service account
Activated service account credentials for: [master@pl-dofroscra-p.iam.gserviceaccount.com]
Enabling APIs
Operation "operations/acf.p2-305055072470-625a52a9-96a7-4e2f-831e-a76491c8882c" finished successfully.
Creating deployment service account with id 'deployment'
Created service account [deployment].
Creating JSON key file for deployment service account at ./gcp-service-account-key.json
created key [fa74d605f5891a6f875a2663b6beeea425730b5b] of type [json] as [./gcp-service-account-key.json] for [deployment@pl-dofroscra-p.iam.gserviceaccount.com]
Adding roles for service account
Updated IAM policy for project [pl-dofroscra-p].
bindings:
# ...
- members:
  - serviceAccount:deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/storage.admin
etag: BwXgw51MPTM=
version: 1
Creating secrets
Created secret [GPG_KEY].
Created version [1] of the secret [GPG_KEY].
Created secret [GPG_PASSWORD].
Created version [1] of the secret [GPG_PASSWORD].
Creating firewall rule to allow HTTP traffic
Creating firewall...
..Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/global/firewalls/default-allow-http].
done.
Creating a Compute Instance VM
Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/zones/us-central1-a/instances/dofroscra-test].
NAME        ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP    STATUS
dofroscra-test  us-central1-a  e2-micro      true         10.128.0.2   34.70.213.101  RUNNING
Activating deployment service account
Activated service account credentials for: [deployment@pl-dofroscra-p.iam.gserviceaccount.com]
Transferring provisioning script
Updating project ssh metadata...
..........................................................................Updated [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p].
.done.
Waiting for SSH key to propagate.
# ...
provision.sh              | 0 kB |   0.7 kB/s | ETA: 00:00:00 | 100%)
Executing provisioning script
Get:1 http://security.debian.org/debian-security bullseye-security InRelease [44.1 kB]
# ...
Processing triggers for man-db (2.9.4-2) ...
Processing triggers for libc-bin (2.31-13+deb11u3) ...
Authenticating docker via gcloud in the VM
# ...
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
Docker configuration file updated.


Provisioning done!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition, I also created a script for the Deployment at  &lt;code&gt;.infrastructure/deploy.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Usage: deploy.sh project_id vm_name"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No project_id given! &lt;/span&gt;&lt;span class="nv"&gt;$usage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No vm_name given! &lt;/span&gt;&lt;span class="nv"&gt;$usage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0;32m"&lt;/span&gt;
&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0m"&lt;/span&gt;

&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1-a
&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gcr.io/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/nginx:latest"&lt;/span&gt;  
&lt;span class="nv"&gt;container_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx
&lt;span class="nv"&gt;deployment_service_account_key_location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gcp-service-account-key.json

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Building nginx docker image with name '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
FROM nginx:1.21.5-alpine

RUN echo "Hello world" &amp;gt;&amp;gt; /usr/share/nginx/html/hello.html
&lt;/span&gt;&lt;span class="no"&gt;
EOF

&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Authenticating docker&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;deployment_service_account_key_location&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | docker login &lt;span class="nt"&gt;-u&lt;/span&gt; _json_key &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; https://gcr.io

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Pushing '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' to registry&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker push &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Transferring deployment script&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud compute scp &lt;span class="nt"&gt;--zone&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; ./.infrastructure/scripts/deploy.sh &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:deploy.sh

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Executing deployment script&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gcloud compute ssh &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bash deploy.sh '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;image_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Retrieving external IP of the VM&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ip_address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud compute instances describe &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;vm_zone&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;project_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'get(networkInterfaces[0].accessConfigs[0].natIP)'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"http://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ip_address&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/hello.html&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Deployment done!&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bash .infrastructure/deploy.sh pl-dofroscra-p dofroscra-test
Building nginx docker image with name 'gcr.io/pl-dofroscra-p/nginx:latest'
#...
#7 writing image sha256:65ff457111599e3f4dd439138b052cff74f10e3332c593178d8288aebb88bb1c done
#7 naming to gcr.io/pl-dofroscra-p/nginx:latest done
#7 DONE 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Authenticating docker
Login Succeeded

Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/
Pushing 'gcr.io/pl-dofroscra-p/nginx:latest' to registry
The push refers to repository [gcr.io/pl-dofroscra-p/nginx]
ad4683501621: Preparing
# ...
1c9c1e42aafa: Pushed
latest: digest: sha256:680086b3c77e4b895099c0a5f6e713ff79ba0d78e1e1df1bc2546d6f979126e4 size: 1775
Transferring deployment script
deploy.sh                 | 0 kB |   0.6 kB/s | ETA: 00:00:00 | 100%
Executing deployment script
Pulling 'gcr.io/pl-dofroscra-p/nginx:latest' from registry
latest: Pulling from pl-dofroscra-p/nginx
Digest: sha256:f17a9092051c389abf76d254e4d564dbd7a814eb21e3cc47b667db301aa9b497
# ...
gcr.io/pl-dofroscra-p/nginx:latest
Starting container
nginx
58c0a34ca44c9bec97c991bdc69d2353ed75f4214f221e444da36e195a215c75
Getting secret GPG_KEY
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQPGBGKA1psBCACq5zYDT587CVZEIWXbUplfAGQZOQJALmzErYpTp0jt+rp4vJhR
U5xahy3pqCq81Cnny5YME50ybB3pW/WcHxWLBDo+he8PKeLbp6wFFjJns+3u4opH
9gFMElyHpzTGiDQYfx/CgY2hKz7GSqpjmnOaKxYvGv0EsbZczyHY1WIN/YFzb0tI
tY7J4zTSH05I+aazRdHyn28QcCRcIT9+4q+5Vk8gz8mmgoqVpyeNgQcqJjcd03iP
WUZd1vZCumOvdG5PZNlc/wPFhqLDmYyLmJ7pt5bWIgty9BjYK8Z2NOdUaekqVEJ+
r29HbzwgFLLE2gd52f07h2y2YgMdWdz4FDxVABEBAAH+BwMC9veBYT2oigXxExLl
7fZKVjw02lEr1NpYd5X1ge9WPU/1qumATJWounzciiETpsYGsbPd9zFRJP4E3JZl
sFSh4p0/kXYTuenYD8wgGkeYyN4lm53IHfqSn2z9JMW5Kz9XEODtKJl8fjcn9Zeb
Getting secret GPG_PASSWORD
87654321
Retrieving external IP of the VM
http://34.70.213.101/hello.html


Deployment done!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="cleanup"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;The easiest way to "cleanup everything" that might create any costs, e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the docker images stored on Cloud Storage&lt;/li&gt;
&lt;li&gt;the secrets in the Secret Manager&lt;/li&gt;
&lt;li&gt;the VM itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;is to  &lt;a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects"&gt;delete the whole project&lt;/a&gt;, e.g. via the &lt;a href="https://console.cloud.google.com/iam-admin/settings"&gt;Settings UI&lt;/a&gt;&lt;/p&gt;


  
Your browser does not support the video tag.


&lt;p&gt;FYI: GCP projects have 30-day-grace-period during that you can "revert" the deletion.&lt;/p&gt;

&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. You should now be familiar enough with GCP to create a Compute Instance VM and  configure it to run dockerized applications on it.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/"&gt;deploy our dockerized PHP application "to production" on GCP via &lt;code&gt;docker compose&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>tutorial</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>CI Pipelines for dockerized PHP Apps with Github &amp; Gitlab [Tutorial Part 7]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Mon, 17 Apr 2023 07:17:19 +0000</pubDate>
      <link>https://dev.to/pascallandau/ci-pipelines-for-dockerized-php-apps-with-github-gitlab-tutorial-part-7-5gc2</link>
      <guid>https://dev.to/pascallandau/ci-pipelines-for-dockerized-php-apps-with-github-gitlab-tutorial-part-7-5gc2</guid>
      <description>&lt;p&gt;How to setup CI (Continuous Integration) pipelines for dockerized PHP applications with Github Actions and Gitlab Pipelines&lt;/p&gt;

&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/" rel="noopener noreferrer"&gt;CI Pipelines for dockerized PHP Apps with Github &amp;amp; Gitlab [Tutorial Part 7]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In the seventh part of this tutorial series on developing PHP on Docker we will &lt;strong&gt;setup a CI (Continuous Integration) pipeline to run code quality tools and tests on Github Actions and Gitlab Pipelines&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/VsNvvt0CMm8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.   You find the branch for this tutorial at &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-7-ci-pipeline-docker-php-gitlab-github" rel="noopener noreferrer"&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/" rel="noopener noreferrer"&gt;Use git-secret to encrypt secrets in the repository&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/" rel="noopener noreferrer"&gt;A primer on GCP Compute Instance VMs for dockerized Apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
Introduction

&lt;ul&gt;
&lt;li&gt;Recommended reading&lt;/li&gt;
&lt;li&gt;Approach&lt;/li&gt;
&lt;li&gt;Try it yourself&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

CI setup

&lt;ul&gt;
&lt;li&gt;
General CI notes

&lt;ul&gt;
&lt;li&gt;Initialize &lt;code&gt;make&lt;/code&gt; for CI&lt;/li&gt;
&lt;li&gt;wait-for-service.sh&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Setup for a "local" CI run

&lt;ul&gt;
&lt;li&gt;Run details&lt;/li&gt;
&lt;li&gt;Execution example&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Setup for Github Actions

&lt;ul&gt;
&lt;li&gt;The Workflow file&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Setup for Gitlab Pipelines

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; pipeline file&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Performance

&lt;ul&gt;
&lt;li&gt;The caching problem on CI&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

Docker changes

&lt;ul&gt;
&lt;li&gt;
Compose file updates

&lt;ul&gt;
&lt;li&gt;docker-compose.local.yml&lt;/li&gt;
&lt;li&gt;docker-compose.ci.yml&lt;/li&gt;
&lt;li&gt;Adding a health check for &lt;code&gt;mysql&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Build target: &lt;code&gt;ci&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
Build stage &lt;code&gt;ci&lt;/code&gt; in the &lt;code&gt;php-base&lt;/code&gt; image

&lt;ul&gt;
&lt;li&gt;Use the whole codebase as build context&lt;/li&gt;
&lt;li&gt;Build the dependencies&lt;/li&gt;
&lt;li&gt;Create the final image&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Build stage &lt;code&gt;ci&lt;/code&gt; in the &lt;code&gt;application&lt;/code&gt; image&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;.dockerignore&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

Makefile changes

&lt;ul&gt;
&lt;li&gt;Initialize the shared variables&lt;/li&gt;
&lt;li&gt;ENV based docker compose config&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Codebase changes

&lt;ul&gt;
&lt;li&gt;Add a test for encrypted files&lt;/li&gt;
&lt;li&gt;Add a password-protected secret &lt;code&gt;gpg&lt;/code&gt; key&lt;/li&gt;
&lt;li&gt;Create a JUnit report from PhpUnit&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;CI is short for &lt;strong&gt;C&lt;/strong&gt;ontinuous &lt;strong&gt;I&lt;/strong&gt;ntegration and to me mostly means &lt;strong&gt;running the code quality tools and tests of a codebase in an isolated environment&lt;/strong&gt; (preferably automatically). This is   particularly important when working in a team, because &lt;strong&gt;the CI system acts as the final gatekeeper&lt;/strong&gt; before features or bugfixes are merged into the main branch.&lt;/p&gt;

&lt;p&gt;I initially learned about CI systems when I stubbed my toes into the open source water. Back in the day I used &lt;a href="https://travis-ci.org/" rel="noopener noreferrer"&gt;Travis CI&lt;/a&gt; for my own projects and replaced it with &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;Github Actions&lt;/a&gt; at some point. At ABOUT YOU we started out with a self-hosted &lt;a href="https://www.jenkins.io/" rel="noopener noreferrer"&gt;Jenkins&lt;/a&gt; server and then moved on to &lt;a href="https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/" rel="noopener noreferrer"&gt;Gitlab CI&lt;/a&gt; as a fully managed solution (though we use &lt;a href="https://docs.gitlab.com/runner/" rel="noopener noreferrer"&gt;custom runners&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a id="recommended-reading"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended reading
&lt;/h3&gt;

&lt;p&gt;This tutorial builds on top of the previous parts. I'll do my best to cross-reference the  corresponding articles when necessary, but I would still recommend to do some upfront reading on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#structuring-the-repository" rel="noopener noreferrer"&gt;general folder structure&lt;/a&gt;, the 
&lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#docker" rel="noopener noreferrer"&gt;update of the &lt;code&gt;.docker/&lt;/code&gt; directory&lt;/a&gt; and the introduction of a 
&lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#make-mk-includes" rel="noopener noreferrer"&gt;&lt;code&gt;.make/&lt;/code&gt; directory&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#makefile-and-bashrc" rel="noopener noreferrer"&gt;general usage of &lt;code&gt;make&lt;/code&gt;&lt;/a&gt; 
and &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#makefile" rel="noopener noreferrer"&gt;it's evolution&lt;/a&gt; as well as 
the &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#make-docker-3" rel="noopener noreferrer"&gt;connection to &lt;code&gt;docker compose&lt;/code&gt; commands&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the concepts of the &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#docker" rel="noopener noreferrer"&gt;docker containers and the &lt;code&gt;docker compose&lt;/code&gt; setup&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And as a nice-to-know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the setup of &lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/#install-phpunit" rel="noopener noreferrer"&gt;PhpUnit for the &lt;code&gt;test&lt;/code&gt; make target&lt;/a&gt; as well as the 
&lt;a href="https://www.pascallandau.com/blog/php-qa-tools-make-docker/#qa-make-targets" rel="noopener noreferrer"&gt;&lt;code&gt;qa&lt;/code&gt; make target&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/" rel="noopener noreferrer"&gt;usage of &lt;code&gt;git-secret&lt;/code&gt; to handle secret values&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="approach"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach
&lt;/h3&gt;

&lt;p&gt;In this tutorial I'm going to explain &lt;strong&gt;how to make our existing docker setup work with Github Actions and &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/" rel="noopener noreferrer"&gt;Gitlab CI/CD Pipelines&lt;/a&gt;&lt;/strong&gt;. As I'm a big fan of a "progressive enhancement" approach, we will ensure that &lt;strong&gt;all necessary steps can be performed  locally through &lt;code&gt;make&lt;/code&gt;&lt;/strong&gt;. This has the additional benefit of keeping a single source of truth (the &lt;code&gt;Makefile&lt;/code&gt;) which will come in handy when we set up the CI system on two different providers (Github and Gitlab).&lt;/p&gt;

&lt;p&gt;The general process will look very similar to the one for local development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build the docker setup&lt;/li&gt;
&lt;li&gt;start the docker setup&lt;/li&gt;
&lt;li&gt;run the qa tools&lt;/li&gt;
&lt;li&gt;run the tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see the final results in the CI setup section, including the concrete &lt;code&gt;yml&lt;/code&gt;  files and links to the repositories, see&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup for a "local" CI run&lt;/li&gt;
&lt;li&gt;Setup for Github Actions&lt;/li&gt;
&lt;li&gt;Setup for Gitlab Pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a code level, we will &lt;strong&gt;treat CI as an environment&lt;/strong&gt;, configured through the env variable &lt;code&gt;ENV&lt;/code&gt;. So far we only used &lt;code&gt;ENV=local&lt;/code&gt; and we will extend that to also use &lt;code&gt;ENV=ci&lt;/code&gt;. The necessary changes  are explained after the concrete CI setup instructions in the sections&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker changes&lt;/li&gt;
&lt;li&gt;Makefile changes&lt;/li&gt;
&lt;li&gt;Codebase changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="try-it-yourself"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;p&gt;To get a feeling for what's going on, you can start by  executing the local CI run: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkout branch &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-7-ci-pipeline-docker-php-gitlab-github" rel="noopener noreferrer"&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;initialize &lt;code&gt;make&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run the &lt;code&gt;.local-ci.sh&lt;/code&gt; script &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This should give you a similar output as presented in the Execution example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout part-7-ci-pipeline-docker-php-gitlab-github

&lt;span class="c"&gt;# Initialize make&lt;/span&gt;
make make-init

&lt;span class="c"&gt;# Execute the local CI run&lt;/span&gt;
bash .local-ci.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="ci-setup"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CI setup
&lt;/h2&gt;

&lt;p&gt;&lt;a id="general-ci-notes"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  General CI notes
&lt;/h3&gt;

&lt;p&gt;&lt;a id="initialize-make-for-ci"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Initialize &lt;code&gt;make&lt;/code&gt; for CI
&lt;/h4&gt;

&lt;p&gt;As a very first step we need to "configure" the codebase to operate for the &lt;code&gt;ci&lt;/code&gt; environment. This is done through the &lt;code&gt;make-init&lt;/code&gt; target as explained later in more detail in the Makefile changes section via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make make-init &lt;span class="nv"&gt;ENVS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
Created a local .make/.env file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ENV=ci&lt;/code&gt; ensures that we&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the correct &lt;code&gt;docker compose&lt;/code&gt; config files
&lt;/li&gt;
&lt;li&gt;use the &lt;code&gt;ci&lt;/code&gt; build target
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;TAG=latest&lt;/code&gt; is just a simplification for now because we don't do anything with the images yet. In an upcoming tutorial we will push them to a container registry for later usage in production deployments and then set the &lt;code&gt;TAG&lt;/code&gt; to something more meaningful (like the build number).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXECUTE_IN_CONTAINER=true&lt;/code&gt; forces every &lt;code&gt;make&lt;/code&gt; command that uses a &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#run-commands-in-the-docker-containers" rel="noopener noreferrer"&gt;&lt;code&gt;RUN_IN_*_CONTAINER&lt;/code&gt; setup&lt;/a&gt; to run in a container. This is important, because &lt;strong&gt;the Gitlab runner will actually run in a docker container itself&lt;/strong&gt;. However, this would cause any affected target &lt;strong&gt;to omit the  &lt;code&gt;$(DOCKER_COMPOSER) exec&lt;/code&gt; prefix&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/execute-always-in-docker.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fexecute-always-in-docker.PNG" alt="Execute all targets in the application docker container"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GPG_PASSWORD=12345678&lt;/code&gt; is the password for the secret &lt;code&gt;gpg&lt;/code&gt; key as mentioned in Add a password-protected secret &lt;code&gt;gpg&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;&lt;a id="wait-for-service-sh"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  wait-for-service.sh
&lt;/h4&gt;

&lt;p&gt;I'll explain the "container is up and running but the underlying service is not" problem for the &lt;code&gt;mysql&lt;/code&gt; service and how we can solve it with a health check later in this article at Adding a health check for &lt;code&gt;mysql&lt;/code&gt;. On purpose, we don't want &lt;code&gt;docker compose&lt;/code&gt; to take care of the waiting because we can make  "better use of the waiting time" and will instead implement it ourselves with a simple bash  script located at &lt;code&gt;.docker/scripts/wait-for-service.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage example: bash wait-for-service.sh mysql 5 1"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for service '&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;' to become healthy, checking every &lt;/span&gt;&lt;span class="nv"&gt;$interval&lt;/span&gt;&lt;span class="s2"&gt; second(s) for max. &lt;/span&gt;&lt;span class="nv"&gt;$max&lt;/span&gt;&lt;span class="s2"&gt; times"&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
  &lt;span class="o"&gt;((&lt;/span&gt;i++&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$max&lt;/span&gt;&lt;span class="s2"&gt;] ..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"{{json .State.Health.Status }}"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"healthy"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then 
   &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SUCCESS"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$max&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then 
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi 
  &lt;/span&gt;&lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nv"&gt;$interval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script waits for a docker &lt;code&gt;$service&lt;/code&gt; to become "healthy" by &lt;a href="https://stackoverflow.com/a/42738182/413531" rel="noopener noreferrer"&gt;checking the &lt;code&gt;.State.Health.Status&lt;/code&gt; info&lt;/a&gt; of the &lt;code&gt;docker inspect&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAUTION:&lt;/strong&gt; The script uses &lt;code&gt;$(docker ps --filter name="$name" -q)&lt;/code&gt; to determine the id of the container, i.e. it will "match" all running containers against the &lt;code&gt;$name&lt;/code&gt; - this would fail if there is more than one matching container! I.e. you must ensure that &lt;code&gt;$name&lt;/code&gt; is specific enough to identify one single container uniquely.&lt;/p&gt;

&lt;p&gt;The script will check up to &lt;code&gt;$max&lt;/code&gt; times in a interval of &lt;code&gt;$interval&lt;/code&gt; seconds. See &lt;a href="https://unix.stackexchange.com/a/82610" rel="noopener noreferrer"&gt;these&lt;/a&gt; &lt;a href="https://unix.stackexchange.com/a/137639" rel="noopener noreferrer"&gt;answers&lt;/a&gt; on the "How do I write a retry logic in script to keep retrying to run it up to 5 times?" question for the implementation of the retry logic. To check the health of the &lt;code&gt;mysql&lt;/code&gt; service for 5 times with 1 seconds between each try, it can be called via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash wait-for-service.sh mysql 5 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 5 times
[1/5] ...
[2/5] ...
[3/5] ...
[4/5] ...
[5/5] ...
FAIL

# OR

$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 5 times
[1/5] ...
[2/5] ...
SUCCESS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem of "container dependencies" isn't new and there are already some existing solutions out there, e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/eficode/wait-for" rel="noopener noreferrer"&gt;wait-for&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vishnubob/wait-for-it" rel="noopener noreferrer"&gt;wait-for-it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jwilder/dockerize#waiting-for-other-dependencies" rel="noopener noreferrer"&gt;dockerize&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ufoscout/docker-compose-wait" rel="noopener noreferrer"&gt;docker-compose-wait&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But unfortunately all of them operate by checking the availability of a &lt;code&gt;host:port&lt;/code&gt; combination and in the case of &lt;code&gt;mysql&lt;/code&gt; that didn't help, because the container was up, the port was reachable but the &lt;code&gt;mysql&lt;/code&gt; service in the container was not.&lt;/p&gt;

&lt;p&gt;&lt;a id="setup-for-a-local-ci-run"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup for a "local" CI run
&lt;/h3&gt;

&lt;p&gt;As mentioned under Approach, we want to be able to perform all necessary steps locally and I created a corresponding script at &lt;code&gt;.local-ci.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# fail on any error &lt;/span&gt;
&lt;span class="c"&gt;# @see https://stackoverflow.com/a/3474556/413531&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

make docker-down &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;&lt;span class="nv"&gt;start_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# STORE GPG KEY&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;secret-protected.gpg.example secret.gpg

&lt;span class="c"&gt;# DEBUG&lt;/span&gt;
docker version
docker compose version
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-release&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# SETUP DOCKER&lt;/span&gt;
make make-init &lt;span class="nv"&gt;ENVS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"&lt;/span&gt;
&lt;span class="nv"&gt;start_docker_build&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
make docker-build
&lt;span class="nv"&gt;end_docker_build&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;777 .build

&lt;span class="c"&gt;# START DOCKER&lt;/span&gt;
&lt;span class="nv"&gt;start_docker_up&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
make docker-up
&lt;span class="nv"&gt;end_docker_up&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
make gpg-init
make secret-decrypt-with-password

&lt;span class="c"&gt;# QA&lt;/span&gt;
&lt;span class="nv"&gt;start_qa&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
make qa &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;end_qa&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# WAIT FOR CONTAINERS&lt;/span&gt;
&lt;span class="nv"&gt;start_wait_for_containers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
bash .docker/scripts/wait-for-service.sh mysql 30 1
&lt;span class="nv"&gt;end_wait_for_containers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# TEST&lt;/span&gt;
&lt;span class="nv"&gt;start_test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
make &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;end_test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;end_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# RUNTIMES&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Build docker:        "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_docker_build&lt;/span&gt; - &lt;span class="nv"&gt;$start_docker_build&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Start docker:        "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_docker_up&lt;/span&gt; - &lt;span class="nv"&gt;$start_docker_up&lt;/span&gt;  &lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"QA:                  "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_qa&lt;/span&gt; - &lt;span class="nv"&gt;$start_qa&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Wait for containers: "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_wait_for_containers&lt;/span&gt; - &lt;span class="nv"&gt;$start_wait_for_containers&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tests:               "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_test&lt;/span&gt; - &lt;span class="nv"&gt;$start_test&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"---------------------"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Total:               "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_total&lt;/span&gt; - &lt;span class="nv"&gt;$start_total&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

&lt;span class="c"&gt;# CLEANUP&lt;/span&gt;
&lt;span class="c"&gt;# reset the default make variables&lt;/span&gt;
make make-init
make docker-down &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# EVALUATE RESULTS&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FAILED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SUCCESS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="run-details"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Run details
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;as a preparation step, we first ensure that no outdated &lt;code&gt;ci&lt;/code&gt; containers are running (this is
only necessary locally, because runners on a remote CI system will start "from scratch")
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  make docker-down &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;we take some time measurements to understand how long certain parts take via
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nv"&gt;start_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to store the current timestamp&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we need the secret &lt;code&gt;gpg&lt;/code&gt; key in order to decrypt the secrets and simply copy the
password-protected example key 
(in the actual CI systems the key will be configured as a secret value that is injected in 
the run)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# STORE GPG KEY&lt;/span&gt;
  &lt;span class="nb"&gt;cp &lt;/span&gt;secret-protected.gpg.example secret.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;I like printing some debugging info in order to understand which exact circumstances
we're dealing with (tbh, this is mostly relevant when setting the CI system up or making
modifications to it)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# DEBUG&lt;/span&gt;
  docker version
  docker compose version
  &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-release&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;for the docker setup, we start with
initializing the environment for &lt;code&gt;ci&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# SETUP DOCKER&lt;/span&gt;
  make make-init &lt;span class="nv"&gt;ENVS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then build the docker setup&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  make docker-build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and finally add a &lt;code&gt;.build/&lt;/code&gt; directory to&lt;br&gt;
  collect the build artifacts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;777 .build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;then, the docker setup is started
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# START DOCKER&lt;/span&gt;
  make docker-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;gpg&lt;/code&gt; is initialized so that&lt;br&gt;
  the secrets can be decrypted&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  make gpg-init
  make secret-decrypt-with-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't need to pass a &lt;code&gt;GPG_PASSWORD&lt;/code&gt; to &lt;code&gt;secret-decrypt-with-password&lt;/code&gt; because we have set&lt;br&gt;
  it up in the previous step as a default value via &lt;code&gt;make-init&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;once the &lt;code&gt;application&lt;/code&gt; container is running, the qa tools are run by invoking the
&lt;a href="https://www.pascallandau.com/blog/php-qa-tools-make-docker/#the-qa-target" rel="noopener noreferrer"&gt;&lt;code&gt;qa&lt;/code&gt; make target&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# QA&lt;/span&gt;
  make qa &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;|| FAILED=true&lt;/code&gt; part makes sure that the script will not be terminated if the checks fail.&lt;br&gt;
  Instead, the fact that a failure happened is "recorded" in the &lt;code&gt;FAILED&lt;/code&gt; variable so that we&lt;br&gt;
  can evaluate it at the end. We don't want the script to stop here because we want the&lt;br&gt;
  following steps to be executed as well (e.g. the tests).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;to mitigate the
"&lt;code&gt;mysql&lt;/code&gt; is not ready" problem, we will now apply the
wait-for-service.sh script
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# WAIT FOR CONTAINERS&lt;/span&gt;
  bash .docker/scripts/wait-for-service.sh mysql 30 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;once &lt;code&gt;mysql&lt;/code&gt; is ready, we can execute the tests via the 
&lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/#install-phpunit" rel="noopener noreferrer"&gt;&lt;code&gt;test&lt;/code&gt; make target&lt;/a&gt; and 
apply the same &lt;code&gt;|| FAILED=true&lt;/code&gt; workaround as for the qa tools
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# TEST&lt;/span&gt;
  make &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;finally, all the timers are printed
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# RUNTIMES&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Build docker:        "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_docker_build&lt;/span&gt; - &lt;span class="nv"&gt;$start_docker_build&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Start docker:        "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_docker_up&lt;/span&gt; - &lt;span class="nv"&gt;$start_docker_up&lt;/span&gt;  &lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"QA:                  "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_qa&lt;/span&gt; - &lt;span class="nv"&gt;$start_qa&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Wait for containers: "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_wait_for_containers&lt;/span&gt; - &lt;span class="nv"&gt;$start_wait_for_containers&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tests:               "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_test&lt;/span&gt; - &lt;span class="nv"&gt;$start_test&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"---------------------"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Total:               "&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;expr&lt;/span&gt; &lt;span class="nv"&gt;$end_total&lt;/span&gt; - &lt;span class="nv"&gt;$start_total&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;we clean up the resources (this is only necessary when running locally, because the runner of
a CI system would be shut down anyway)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# CLEANUP&lt;/span&gt;
  make make-init
  make docker-down &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;and finally evaluate if any error occurred when running the qa tools or the tests
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# EVALUATE RESULTS&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FAILED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SUCCESS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;a id="execution-example"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  Execution example
&lt;/h4&gt;

&lt;p&gt;Executing the script via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash .local-ci.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;yields the following (shortened) output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bash .local-ci.sh
Container dofroscra_ci-redis-1  Stopping
# Stopping all other `ci` containers ...
# ...

Client:
 Cloud integration: v1.0.22
 Version:           20.10.13
# Print more debugging info ...
# ...

Created a local .make/.env file
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose-php-base.yml build php-base
#1 [internal] load build definition from Dockerfile
# Output from building the docker containers 
# ...

ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.yml -f ./.docker/docker-compose/docker-compose.ci.yml up -d
Network dofroscra_ci_network  Creating
# Starting all `ci` containers ...
# ...

"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES="secret.gpg"
gpg: directory '/home/application/.gnupg' created
gpg: keybox '/home/application/.gnupg/pubring.kbx' created
gpg: /home/application/.gnupg/trustdb.gpg: trustdb created
gpg: key D7A860BBB91B60C7: public key "Alice Doe protected &amp;lt;alice.protected@example.com&amp;gt;" imported
# Output of importing the secret and public gpg keys
# ...

"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f -p 12345678"
git-secret: done. 1 of 1 files are revealed.
"C:/Program Files/Git/mingw64/bin/make" -j 8 -k --no-print-directory --output-sync=target qa-exec NO_PROGRESS=true
phplint                             done   took 4s
phpcs                               done   took 4s
phpstan                             done   took 8s
composer-require-checker            done   took 8s
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 30 times
[1/30] ...
SUCCESS
PHPUnit 9.5.19 #StandWithUkraine

........                                                            8 / 8 (100%)

Time: 00:03.077, Memory: 28.00 MB

OK (8 tests, 15 assertions)
Build docker:         12
Start docker:         2
QA:                   9
Wait for containers:  3
Tests:                5
---------------------
Total:                46
Created a local .make/.env file

Container dofroscra_ci-application-1  Stopping
Container dofroscra_ci-mysql-1  Stopping
# Stopping all other `ci` containers ...
# ...

SUCCESS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="setup-for-github-actions"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup for Github Actions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-7-ci-pipeline-docker-php-gitlab-github" rel="noopener noreferrer"&gt;Repository (branch &lt;code&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paslandau/docker-php-tutorial/actions" rel="noopener noreferrer"&gt;CI/CD overview (Actions)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paslandau/docker-php-tutorial/runs/5866235820?check_suite_focus=true" rel="noopener noreferrer"&gt;Example of a successful job&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paslandau/docker-php-tutorial/runs/5867485802?check_suite_focus=true" rel="noopener noreferrer"&gt;Example of a failed job&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/github-action-example.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgithub-action-example.PNG" alt="Github Action example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are completely new to Github Actions, I recommend to start with the &lt;a href="https://docs.github.com/en/actions/quickstart" rel="noopener noreferrer"&gt;official Quickstart Guide for GitHub Actions&lt;/a&gt; and the &lt;a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions" rel="noopener noreferrer"&gt;Understanding GitHub Actions&lt;/a&gt; article. In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Github Actions are based on so called &lt;strong&gt;Workflows&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Workflows are &lt;code&gt;yaml&lt;/code&gt; files that  live in the special &lt;code&gt;.github/workflows&lt;/code&gt; directory in the
repository&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;a Workflow can contain multiple &lt;strong&gt;Jobs&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;each Job consists of a series of &lt;strong&gt;Steps&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;each Step needs a &lt;code&gt;run:&lt;/code&gt; element that represents a command that is executed by a new shell&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-line commands that should use the same shell are written as
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;echo "line 1"&lt;/span&gt;
        &lt;span class="s"&gt;echo "line 2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;See also &lt;a href="https://stackoverflow.com/a/59536836/413531" rel="noopener noreferrer"&gt;difference between "run |" and multiple runs in github actions&lt;/a&gt;&lt;/p&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="the-workflow-file"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The Workflow file
&lt;/h4&gt;

&lt;p&gt;Github Actions are triggered automatically based on the files in the &lt;code&gt;.github/workflows&lt;/code&gt; directory. I have added the file &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI build and test&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# automatically run for pull request and for pushes to branch "part-7-ci-pipeline-docker-php-gitlab-github"&lt;/span&gt;
  &lt;span class="c1"&gt;# @see https://stackoverflow.com/a/58142412/413531&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="c1"&gt;# enable to trigger the action manually&lt;/span&gt;
  &lt;span class="c1"&gt;# @see https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/&lt;/span&gt;
  &lt;span class="c1"&gt;# CAUTION: there is a known bug that makes the "button to trigger the run" not show up&lt;/span&gt;
  &lt;span class="c1"&gt;# @see https://github.community/t/workflow-dispatch-workflow-not-showing-in-actions-tab/130088/29&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;start timer&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "START_TOTAL=$(date +%s)" &amp;gt; $GITHUB_ENV&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STORE GPG KEY&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Note: make sure to wrap the secret in double quotes (")&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.GPG_KEY }}" &amp;gt; ./secret.gpg&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SETUP TOOLS&lt;/span&gt;
        &lt;span class="na"&gt;run &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}&lt;/span&gt;
          &lt;span class="s"&gt;# install docker compose&lt;/span&gt;
          &lt;span class="s"&gt;# @see https://docs.docker.com/compose/cli-command/#install-on-linux&lt;/span&gt;
          &lt;span class="s"&gt;# @see https://github.com/docker/compose/issues/8630#issuecomment-1073166114&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p $DOCKER_CONFIG/cli-plugins &lt;/span&gt;
          &lt;span class="s"&gt;curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose&lt;/span&gt;
          &lt;span class="s"&gt;chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEBUG&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;docker compose version&lt;/span&gt;
          &lt;span class="s"&gt;docker --version&lt;/span&gt;
          &lt;span class="s"&gt;cat /etc/*-release&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SETUP DOCKER&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"&lt;/span&gt;
          &lt;span class="s"&gt;make docker-build&lt;/span&gt;
          &lt;span class="s"&gt;mkdir .build &amp;amp;&amp;amp; chmod 777 .build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;START DOCKER&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;make docker-up&lt;/span&gt;
          &lt;span class="s"&gt;make gpg-init&lt;/span&gt;
          &lt;span class="s"&gt;make secret-decrypt-with-password&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;QA&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Run the tests and qa tools but only store the error instead of failing immediately&lt;/span&gt;
          &lt;span class="s"&gt;# @see https://stackoverflow.com/a/59200738/413531&lt;/span&gt;
          &lt;span class="s"&gt;make qa || echo "FAILED=qa" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WAIT FOR CONTAINERS&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# We need to wait until mysql is available.&lt;/span&gt;
          &lt;span class="s"&gt;bash .docker/scripts/wait-for-service.sh mysql 30 1 &lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEST&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;make test || echo "FAILED=test $FAILED" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUNTIMES&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo `expr $(date +%s) - $START_TOTAL`&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EVALUATE&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Check if $FAILED is NOT empty&lt;/span&gt;
          &lt;span class="s"&gt;if [ ! -z "$FAILED" ]; then echo "Failed at $FAILED" &amp;amp;&amp;amp; exit 1; fi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upload build artifacts&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-artifacts&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The steps are essentially the same as explained before at  Run details for the local run. Some additional notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I want the Action to be triggered automatically only when I
&lt;a href="https://stackoverflow.com/a/58142412/413531" rel="noopener noreferrer"&gt;push to branch &lt;code&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/code&gt;&lt;/a&gt;
OR when a pull request is created (via &lt;code&gt;pull_request&lt;/code&gt;). In addition, I want to be able to
&lt;a href="https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/" rel="noopener noreferrer"&gt;trigger the Action manually on any branch&lt;/a&gt;
(via &lt;code&gt;workflow_dispatch&lt;/code&gt;).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/span&gt;
    &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a real project, I would let the action only run automatically on long-living branches like&lt;br&gt;
  &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;develop&lt;/code&gt;. The manual trigger is helpful if you just want to test your current work&lt;br&gt;
  without putting it up for review. &lt;strong&gt;CAUTION:&lt;/strong&gt; There is a&lt;br&gt;
  &lt;a href="https://github.community/t/workflow-dispatch-workflow-not-showing-in-actions-tab/130088/29" rel="noopener noreferrer"&gt;known issue that "hides" the "Trigger workflow" button to trigger the action manually&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new shell is started for each &lt;code&gt;run:&lt;/code&gt; instruction, thus we must store our timer in the "global"
&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable" rel="noopener noreferrer"&gt;environment variable &lt;code&gt;$GITHUB_ENV&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;start timer&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;echo "START_TOTAL=$(date +%s)" &amp;gt; $GITHUB_ENV &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This will be the only timer we use, because the job uses multiple steps that are timed&lt;br&gt;
  automatically - so we don't need to take timestamps manually:&lt;br&gt;
  &lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/github-action-step-times.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgithub-action-step-times.PNG" alt="Github Action step times"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;gpg&lt;/code&gt; key is configured as an
&lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer"&gt;encrypted secret&lt;/a&gt; named
&lt;code&gt;GPG_KEY&lt;/code&gt; and is stored in &lt;code&gt;./secret.gpg&lt;/code&gt;. The value is the content of the
&lt;code&gt;secret-protected.gpg.example&lt;/code&gt; file
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STORE GPG KEY&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.GPG_KEY }}" &amp;gt; ./secret.gpg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Secrets are configured in the Github repository under &lt;code&gt;Settings &amp;gt; Secrets &amp;gt; Actions&lt;/code&gt; at&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  https://github.com/$user/$repository/settings/secrets/actions

  e.g.

  https://github.com/paslandau/docker-php-tutorial/settings/secrets/actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/github-secrets-ui.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgithub-secrets-ui.PNG" alt="Github Action Secrets UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;ubuntu-latest&lt;/code&gt; image doesn't contain the &lt;code&gt;docker compose&lt;/code&gt; plugin, thus we need to
&lt;a href="https://docs.docker.com/compose/cli-command/#install-on-linux" rel="noopener noreferrer"&gt;install it manually&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SETUP TOOLS&lt;/span&gt;
      &lt;span class="na"&gt;run &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}&lt;/span&gt;
        &lt;span class="s"&gt;mkdir -p $DOCKER_CONFIG/cli-plugins &lt;/span&gt;
        &lt;span class="s"&gt;curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose&lt;/span&gt;
        &lt;span class="s"&gt;chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;for the &lt;code&gt;make&lt;/code&gt; initialization we need the second secret named &lt;code&gt;GPG_PASSWORD&lt;/code&gt; - which is
configured as &lt;code&gt;12345678&lt;/code&gt; in our case, see
Add a password-protected secret gpg key
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SETUP DOCKER&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;because the runner will be shutdown after the run, we need to move the build artifacts to a
permanent location, using the
&lt;a href="https://github.com/actions/upload-artifact#upload-an-entire-directory" rel="noopener noreferrer"&gt;actions/upload-artifact@v3 action&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upload build artifacts&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-artifacts&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can&lt;br&gt;
  &lt;a href="https://github.com/actions/upload-artifact#where-does-the-upload-go" rel="noopener noreferrer"&gt;download the artifacts in the Run overview UI&lt;/a&gt;&lt;br&gt;
  &lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/github-action-run-overview-build-artifacts.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgithub-action-run-overview-build-artifacts.PNG" alt="Github Actions: Run overview UI shows build-artifacts"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;a id="setup-for-gitlab-pipelines"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Setup for Gitlab Pipelines
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/tree/part-7-ci-pipeline-docker-php-gitlab-github" rel="noopener noreferrer"&gt;Repository (branch &lt;code&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines" rel="noopener noreferrer"&gt;CI/CD overview (Pipelines)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines/511339886" rel="noopener noreferrer"&gt;Example of a successful job&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines/511341545" rel="noopener noreferrer"&gt;Example of a failed job&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/gitlab-pipeline-example.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgitlab-pipeline-example.PNG" alt="Gitlab Pipeline example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are completely new to Gitlab Pipelines, I recommend to start with the &lt;a href="https://docs.gitlab.com/ee/ci/quick_start/" rel="noopener noreferrer"&gt;official Get started with GitLab CI/CD guide&lt;/a&gt;. In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the core concept of Gitlab Pipelines is the &lt;strong&gt;Pipeline&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;it is defined in the &lt;code&gt;yaml&lt;/code&gt; file &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; that lives in the root of the repository&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;a Pipeline can contain multiple &lt;strong&gt;Stages&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;each Stage consists of a series of &lt;strong&gt;Jobs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;each Job contains a &lt;a href="https://docs.gitlab.com/ee/ci/yaml/index.html#script" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;script&lt;/code&gt;&lt;/strong&gt; section&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;script&lt;/code&gt; section consists of a series of shell commands&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;a id="the-gitlab-ci-yml-pipeline-file"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  The &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; pipeline file
&lt;/h4&gt;

&lt;p&gt;Gitlab Pipelines are triggered automatically based on a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file located at the root of the repository. It has the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build_test&lt;/span&gt;

&lt;span class="na"&gt;QA and Tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build_test&lt;/span&gt;

  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# automatically run for pull request and for pushes to branch "part-7-ci-pipeline-docker-php-gitlab-github"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;($CI_PIPELINE_SOURCE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"merge_request_event"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"part-7-ci-pipeline-docker-php-gitlab-github")'&lt;/span&gt;

  &lt;span class="c1"&gt;# see https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker:20.10.12&lt;/span&gt;

  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker:dind&lt;/span&gt;

  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_total=$(date +%s)&lt;/span&gt;

    &lt;span class="c1"&gt;## STORE GPG KEY&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cp $GPG_KEY_FILE ./secret.gpg&lt;/span&gt;

    &lt;span class="c1"&gt;## SETUP TOOLS&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_install_tools=$(date +%s)&lt;/span&gt;
    &lt;span class="c1"&gt;# "curl" is required to download docker compose&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apk add --no-cache make bash curl&lt;/span&gt;
    &lt;span class="c1"&gt;# install docker compose&lt;/span&gt;
    &lt;span class="c1"&gt;# @see https://docs.docker.com/compose/cli-command/#install-on-linux&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p ~/.docker/cli-plugins/&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chmod +x ~/.docker/cli-plugins/docker-compose&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_install_tools=$(date +%s)&lt;/span&gt;

    &lt;span class="c1"&gt;## DEBUG&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker version&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker compose version&lt;/span&gt;
    &lt;span class="c1"&gt;# show linux distro info&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat /etc/*-release&lt;/span&gt;

    &lt;span class="c1"&gt;## SETUP DOCKER&lt;/span&gt;
    &lt;span class="c1"&gt;# Pass default values to the make-init command - otherwise we would have to pass those as arguments to every make call&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=$GPG_PASSWORD"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_docker_build=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make docker-build&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_docker_build=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir .build &amp;amp;&amp;amp; chmod 777 .build&lt;/span&gt;

    &lt;span class="c1"&gt;## START DOCKER&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_docker_up=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make docker-up&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_docker_up=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make gpg-init&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make secret-decrypt-with-password&lt;/span&gt;

    &lt;span class="c1"&gt;## QA&lt;/span&gt;
    &lt;span class="c1"&gt;# Run the tests and qa tools but only store the error instead of failing immediately&lt;/span&gt;
    &lt;span class="c1"&gt;# @see https://stackoverflow.com/a/59200738/413531&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_qa=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make qa ENV=ci || FAILED=true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_qa=$(date +%s)&lt;/span&gt;

    &lt;span class="c1"&gt;## WAIT FOR CONTAINERS&lt;/span&gt;
    &lt;span class="c1"&gt;# We need to wait until mysql is available.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_wait_for_containers=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bash .docker/scripts/wait-for-service.sh mysql 30 &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_wait_for_containers=$(date +%s)&lt;/span&gt;

    &lt;span class="c1"&gt;## TEST&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start_test=$(date +%s)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make test ENV=ci || FAILED=true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_test=$(date +%s)&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_total=$(date +%s)&lt;/span&gt;

    &lt;span class="c1"&gt;# RUNTIMES&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Tools:" `expr $end_install_tools - $start_install_tools`&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Build docker:" `expr $end_docker_build - $start_docker_build`&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Start docker:" `expr $end_docker_up - $start_docker_up  `&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "QA:" `expr $end_qa - $start_qa`&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Wait for containers:" `expr $end_wait_for_containers - $start_wait_for_containers`&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Tests:" `expr $end_test - $start_test`&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Total:" `expr $end_total - $start_total`&lt;/span&gt;

    &lt;span class="c1"&gt;# EVALUATE RESULTS&lt;/span&gt;
    &lt;span class="c1"&gt;# Use if-else constructs in Gitlab pipelines&lt;/span&gt;
    &lt;span class="c1"&gt;# @see https://stackoverflow.com/a/55464100/413531&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;if [ "$FAILED" == "true" ]; then exit 1; fi&lt;/span&gt;

  &lt;span class="c1"&gt;# Save the build artifact, e.g. the JUNIT report.xml file, so we can download it later&lt;/span&gt;
  &lt;span class="c1"&gt;# @see https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# the quotes are required&lt;/span&gt;
      &lt;span class="c1"&gt;# @see https://stackoverflow.com/questions/38009869/how-to-specify-wildcard-artifacts-subdirectories-in-gitlab-ci-yml#comment101411265_38055730&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.build/*"&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 week&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The steps are essentially the same as explained before under  Run details for the local run. Some additional notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we start by defining the stages of the pipeline - though that's currently just one (&lt;code&gt;build_test&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build_test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;then we define the job &lt;code&gt;QA and Tests&lt;/code&gt; and assign it to the &lt;code&gt;build_test&lt;/code&gt; stage
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;QA and Tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build_test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;I want the Pipeline to be triggered automatically only when I
&lt;a href="https://stackoverflow.com/a/66812732/413531" rel="noopener noreferrer"&gt;push to branch &lt;code&gt;part-7-ci-pipeline-docker-php-gitlab-github&lt;/code&gt;&lt;/a&gt;
OR &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/merge_request_pipelines.html#use-rules-to-add-jobs" rel="noopener noreferrer"&gt;when a pull request is created&lt;/a&gt;
&lt;a href="https://www.shellhacks.com/gitlab-ci-cd-trigger-pipeline-manually-api/" rel="noopener noreferrer"&gt;Triggering the Pipeline manually on any branch is possible by default&lt;/a&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;($CI_PIPELINE_SOURCE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"merge_request_event"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"part-7-ci-pipeline-docker-php-gitlab-github")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;since we want to build and run docker images, we need to use a docker base image and activate the
&lt;code&gt;docker:dind&lt;/code&gt; service. See &lt;a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker" rel="noopener noreferrer"&gt;Use Docker to build Docker images: Use Docker-in-Docker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker:20.10.12&lt;/span&gt;

  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker:dind&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;we store the secret &lt;code&gt;gpg&lt;/code&gt; key as a secret file (using the
&lt;a href="https://docs.gitlab.com/ee/ci/variables/#cicd-variable-types" rel="noopener noreferrer"&gt;"file" type&lt;/a&gt;) in the
&lt;a href="https://docs.gitlab.com/ee/ci/variables/#custom-cicd-variables" rel="noopener noreferrer"&gt;CI/CD variables configuration of the Gitlab repository&lt;/a&gt;
and move it to &lt;code&gt;./secret.gpg&lt;/code&gt; in order to decrypt the secrets later
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="c1"&gt;## STORE GPG KEY&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cp $GPG_KEY_FILE ./secret.gpg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secrets can be configured under &lt;code&gt;Settings &amp;gt; CI/CD &amp;gt; Variables&lt;/code&gt; at&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  https://gitlab.com/$project/$repository/-/settings/ci_cd

  e.g.

  https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/settings/ci_cd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/gitlab-ci-cd-variables-ui.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgitlab-ci-cd-variables-ui.PNG" alt="Gitlab CI/CD Variables UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the docker base image doesn't come with all required tools, thus we need to install the
missing ones (&lt;code&gt;make&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;docker compose&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="c1"&gt;## SETUP TOOLS&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apk add --no-cache make bash curl&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p ~/.docker/cli-plugins/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;chmod +x ~/.docker/cli-plugins/docker-compose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;for the initialization of &lt;code&gt;make&lt;/code&gt; we use the &lt;code&gt;$GPG_PASSWORD&lt;/code&gt; variable that we defined in the
CI/CD settings
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="c1"&gt;## SETUP DOCKER&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=$GPG_PASSWORD"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: I have &lt;a href="https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable" rel="noopener noreferrer"&gt;marked the variable as "masked"&lt;/a&gt;&lt;br&gt;
  so it won't show up in any logs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;finally, we store &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html" rel="noopener noreferrer"&gt;the job artifacts&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.build/*"&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 week&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;They can be accessed in the &lt;a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines" rel="noopener noreferrer"&gt;Pipeline overview UI&lt;/a&gt;&lt;br&gt;
  &lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/gitlab-pipeline-build-artifacts.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fgitlab-pipeline-build-artifacts.PNG" alt="Gitlab Pipeline overview UI"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;a id="performance"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/aGWGJQWtH1I"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance isn't an issue right now&lt;/strong&gt;, because the CI runs take only about ~1 min (Github Actions) and ~2 min (Gitlab Pipelines), but that's mostly because we only ship a super minimal application and those times &lt;em&gt;will go up&lt;/em&gt; when things get more complex. For the local setup I  used all 8 cores of my laptop. The time breakdown is roughly as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Gitlab&lt;/th&gt;
&lt;th&gt;Github&lt;/th&gt;
&lt;th&gt;local &lt;br&gt; without cache&lt;/th&gt;
&lt;th&gt;local &lt;br&gt; with cached images&lt;/th&gt;
&lt;th&gt;local &lt;br&gt; with cached images + layers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SETUP TOOLS&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SETUP DOCKER&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;START DOCKER&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QA&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAIT FOR CONTAINERS&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TESTS&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;total &lt;br&gt; (excl. runner startup)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;total &lt;br&gt; (incl. runner startup)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;139&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Times taken from&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paslandau/docker-php-tutorial/actions/runs/2108659089" rel="noopener noreferrer"&gt;"CI build and test #83" Github Action run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines/511355192" rel="noopener noreferrer"&gt;"Pipeline #511355192" Gitlab Pipeline run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;"local without cache" via &lt;code&gt;bash .local-ci.sh&lt;/code&gt; with no local images at all&lt;/li&gt;
&lt;li&gt;"local with cached images" via &lt;code&gt;bash .local-ci.sh&lt;/code&gt; with cached images for &lt;code&gt;mysql&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"local with cached images + layers" via &lt;code&gt;bash .local-ci.sh&lt;/code&gt; with cached images for &lt;code&gt;mysql&lt;/code&gt; and
&lt;code&gt;redis&lt;/code&gt; and a "warm" layer cache for the &lt;code&gt;application&lt;/code&gt; image
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Optimizing the performance is out of scope for this tutorial&lt;/strong&gt;, but I'll at least document my current findings.&lt;/p&gt;

&lt;p&gt;&lt;a id="the-caching-problem-on-ci"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The caching problem on CI
&lt;/h4&gt;

&lt;p&gt;A good chunk of time is &lt;strong&gt;usually spent on building the docker images&lt;/strong&gt;. We did our best to optimize the process by leveraging the layer cache and using cache mounts (see section Build stage &lt;code&gt;ci&lt;/code&gt; in the &lt;code&gt;php-base&lt;/code&gt; image).  But those steps are futile on CI systems, because the corresponding &lt;strong&gt;runners will start "from  scratch" for every CI run&lt;/strong&gt; - i.e. &lt;strong&gt;there is no local cache&lt;/strong&gt; that they could use. In  consequence, &lt;strong&gt;the full docker setup is also built "from scratch"&lt;/strong&gt; on every run.&lt;/p&gt;

&lt;p&gt;There are ways to mitigate that e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pushing images to a container registry and pulling them before building the images to leverage
the layer cache via the &lt;a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#cache_from" rel="noopener noreferrer"&gt;&lt;code&gt;cache_from&lt;/code&gt; option&lt;/a&gt;
of &lt;code&gt;docker compose&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;exporting and importing the images as &lt;code&gt;tar&lt;/code&gt; archives via
&lt;a href="https://docs.docker.com/engine/reference/commandline/save/" rel="noopener noreferrer"&gt;&lt;code&gt;docker save&lt;/code&gt;&lt;/a&gt; and
&lt;a href="https://docs.docker.com/engine/reference/commandline/load/" rel="noopener noreferrer"&gt;&lt;code&gt;docker load&lt;/code&gt;&lt;/a&gt;,
storing them either in the built-in cache of
&lt;a href="https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows" rel="noopener noreferrer"&gt;Github&lt;/a&gt;
or &lt;a href="https://docs.gitlab.com/ee/ci/caching/" rel="noopener noreferrer"&gt;Gitlab&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;see also the &lt;a href="https://github.com/marketplace/actions/docker-layer-caching" rel="noopener noreferrer"&gt;satackey/action-docker-layer-caching@v0.0.11 Github Action&lt;/a&gt;
and the official &lt;a href="https://github.com/actions/cache" rel="noopener noreferrer"&gt;actions/cache@v3 Github Action&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;using the &lt;a href="https://docs.docker.com/engine/reference/commandline/buildx_build/#cache-from" rel="noopener noreferrer"&gt;&lt;code&gt;--cache-from&lt;/code&gt;&lt;/a&gt;
and &lt;a href="https://docs.docker.com/engine/reference/commandline/buildx_build/#cache-to" rel="noopener noreferrer"&gt;&lt;code&gt;--cache-to&lt;/code&gt;&lt;/a&gt; options of
&lt;a href="https://docs.docker.com/buildx/working-with-buildx/" rel="noopener noreferrer"&gt;&lt;code&gt;buildx&lt;/code&gt;&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;see also the &lt;a href="https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md" rel="noopener noreferrer"&gt;"cache" docu of the docker/build-push-action@v2 Github Action&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;But: None of that worked for me out-of-the-box :( We will take a closer look in an upcoming tutorial. Some reading material that I found valuable so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/dtinth/caching-docker-builds-in-github-actions-which-approach-is-the-fastest-a-research-18ei"&gt;Caching Docker builds in GitHub Actions: Which approach is the fastest? 🤔 A research.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seankhliao.com/blog/12021-01-23-docker-buildx-caching/" rel="noopener noreferrer"&gt;Caching strategies for CI systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://evilmartians.com/chronicles/build-images-on-github-actions-with-docker-layer-caching" rel="noopener noreferrer"&gt;Build images on GitHub Actions with Docker layer caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://testdriven.io/blog/faster-ci-builds-with-docker-cache/" rel="noopener noreferrer"&gt;Faster CI Builds with Docker Layer Caching and BuildKit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/image-rebase-and-improved-remote-cache-support-in-new-buildkit/" rel="noopener noreferrer"&gt;Image rebase and improved remote cache support in new BuildKit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="docker-changes"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker changes
&lt;/h2&gt;

&lt;p&gt;As a first step we need to decide &lt;strong&gt;which containers are required&lt;/strong&gt; and &lt;strong&gt;how to provide the codebase&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since our goal is running the qa tools and tests, we only need the &lt;code&gt;application&lt;/code&gt; php container. The tests also need a database and a queue, i.e. the &lt;code&gt;mysql&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; containers are required as  well - whereas &lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;php-fpm&lt;/code&gt; and &lt;code&gt;php-worker&lt;/code&gt; are not required. We'll handle that through  dedicated &lt;code&gt;docker compose&lt;/code&gt; configuration files that only contain the necessary services. This is  explained in more detail in section Compose file updates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/build-ci-images.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fbuild-ci-images.PNG" alt="Build images for CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our local setup, we have &lt;strong&gt;sheen the host system and docker&lt;/strong&gt; - mainly because we wanted our changes to be reflected immediately in docker. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/share-codebase-bind-mount.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fshare-codebase-bind-mount.PNG" alt="Share the codebase between host system and docker container"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This isn't necessary for the CI&lt;/strong&gt; use case. In fact we want our &lt;strong&gt;CI images as close as  possible to our production images&lt;/strong&gt; - and those should "contain everything to run independently". I.e. &lt;strong&gt;the codebase should live in the image&lt;/strong&gt; - not on the host system. This will be explained  in section Use the whole codebase as build context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fcodebase-in-docker-image.PNG" alt="Add the codebase in the docker image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="compose-file-updates"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Compose file updates
&lt;/h3&gt;

&lt;p&gt;We will not only have some differences between the CI docker setup and the local docker setup (=different containers), but also in the configuration of the individual services. To accommodate for that, we will use the following &lt;code&gt;docker compose&lt;/code&gt; config files in the  &lt;code&gt;.docker/docker-compose/&lt;/code&gt; directory: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker-compose.local.ci.yml&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;holds configuration that is valid for &lt;code&gt;local&lt;/code&gt; and &lt;code&gt;ci&lt;/code&gt;, trying to keep the config files 
&lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" rel="noopener noreferrer"&gt;DRY&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;docker-compose.ci.yml&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;holds configuration that is only valid for &lt;code&gt;ci&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;docker-compose.local.yml&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;holds configuration that is only valid for &lt;code&gt;local&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;When using &lt;code&gt;docker compose&lt;/code&gt; we then need to make sure to include only the required files, e.g. for  &lt;code&gt;ci&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.local.ci.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll explain  the logic for that later in section ENV based docker compose config. In short:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/assemble-docker-compose-files.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fassemble-docker-compose-files.PNG" alt="Assemble docker-compose config files for CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="docker-compose-local-yml"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  docker-compose.local.yml
&lt;/h4&gt;

&lt;p&gt;When comparing &lt;code&gt;ci&lt;/code&gt; with &lt;code&gt;local&lt;/code&gt;, for &lt;code&gt;ci&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we &lt;strong&gt;don't need to share the codebase&lt;/strong&gt; with the host system
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${APP_CODE_PATH_HOST?}:${APP_CODE_PATH_CONTAINER?}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;we &lt;strong&gt;don't need persistent volumes&lt;/strong&gt; for the redis and mysql data
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql:/var/lib/mysql&lt;/span&gt;

    &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;we &lt;strong&gt;don't need to share ports&lt;/strong&gt; with the host system
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${APPLICATION_SSH_HOST_PORT:-2222}:22"&lt;/span&gt;

    &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDIS_HOST_PORT:-6379}:6379"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;we &lt;strong&gt;don't need any settings for local dev tools&lt;/strong&gt; like &lt;code&gt;xdebug&lt;/code&gt; or &lt;code&gt;strace&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}&lt;/span&gt;
      &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SYS_PTRACE"&lt;/span&gt;
      &lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seccomp=unconfined"&lt;/span&gt;
      &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;host.docker.internal:host-gateway&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So all of those config values will only live in the &lt;code&gt;docker-compose.local.yml&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;&lt;a id="docker-compose-ci-yml"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  docker-compose.ci.yml
&lt;/h4&gt;

&lt;p&gt;In fact, there are only two things that &lt;code&gt;ci&lt;/code&gt; needs that &lt;code&gt;local&lt;/code&gt; doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bind mount to &lt;strong&gt;share only the secret gpg key from the host with the &lt;code&gt;application&lt;/code&gt; container&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${APP_CODE_PATH_HOST?}/secret.gpg:${APP_CODE_PATH_CONTAINER?}/secret.gpg:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This&lt;br&gt;
  is &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#local-git-secret-and-gpg-setup" rel="noopener noreferrer"&gt;required to decrypt the secrets&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[...] the private key has to be named &lt;code&gt;secret.gpg&lt;/code&gt; and put in the root of the codebase,&lt;br&gt;
so that the import can be simplified with &lt;code&gt;make&lt;/code&gt; targets&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The secret files themselves are baked into the image, but the key to decrypt them will be &lt;br&gt;
  provided only during runtime and &lt;br&gt;
  &lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image-share-secret-key.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fcodebase-in-docker-image-share-secret-key.PNG" alt="Add the codebase in the docker image and share a secret key file"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bind mount to &lt;strong&gt;share a &lt;code&gt;.build&lt;/code&gt; folder for build artifacts with the &lt;code&gt;application&lt;/code&gt; container&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${APP_CODE_PATH_HOST?}/.build:${APP_CODE_PATH_CONTAINER?}/.build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This will be used to collect any files we want to retain from a build (e.g. code coverage&lt;br&gt;
  information, log files, etc.)&lt;/p&gt;



&lt;p&gt;&lt;a id="adding-a-health-check-for-mysql"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  Adding a health check for &lt;code&gt;mysql&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;When running the tests for the first time on a CI system, I noticed some weird errors related to the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1) Tests\Feature\App\Http\Controllers\HomeControllerTest::test___invoke with data set "default" (array(), '    &amp;lt;li&amp;gt;&amp;lt;a href="?dispatch=fo...&amp;gt;&amp;lt;/li&amp;gt;')
PDOException: SQLSTATE[HY000] [2002] Connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As it turned out, the &lt;code&gt;mysql&lt;/code&gt; container itself was up and running - but the &lt;code&gt;mysql&lt;/code&gt; process &lt;em&gt;within&lt;/em&gt; the container was not yet ready to accept connections. Locally, this hasn't been a problem, because we usually would not run the tests "immediately" after starting the containers - but on CI  this is the case.&lt;/p&gt;

&lt;p&gt;Fortunately, &lt;code&gt;docker compose&lt;/code&gt; has us covered here and provides a &lt;a href="https://docs.docker.com/compose/compose-file/#healthcheck" rel="noopener noreferrer"&gt;&lt;code&gt;healtcheck&lt;/code&gt; configuration option&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;healthcheck&lt;/code&gt; declares a check that’s run to determine whether or not containers for this service are "healthy".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since this &lt;code&gt;healthcheck&lt;/code&gt; is also "valid" for &lt;code&gt;local&lt;/code&gt;, I defined it in the combined  &lt;code&gt;docker-compose.local.ci.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Only mark the service as healthy if mysql is ready to accept connections&lt;/span&gt;
      &lt;span class="c1"&gt;# Check every 2 seconds for 30 times, each check has a timeout of 1s&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script in &lt;code&gt;test&lt;/code&gt; was taken from &lt;a href="https://stackoverflow.com/a/54854239/413531" rel="noopener noreferrer"&gt;SO: Docker-compose check if mysql connection is ready&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When starting the docker setup, &lt;code&gt;docker ps&lt;/code&gt; will now add a health info to the &lt;code&gt;STATUS&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make docker-up

$ docker ps
CONTAINER ID   IMAGE                            STATUS                           NAMES
b509eb2f99c0   dofroscra/application-ci:latest  Up 1 seconds                     dofroscra_ci-application-1
503e52fd9e68   mysql:8.0.28                     Up 1 seconds (health: starting)  dofroscra_ci-mysql-1

# a couple of seconds later

$ docker ps
CONTAINER ID   IMAGE                            STATUS                   NAMES
b509eb2f99c0   dofroscra/application-ci:latest  Up 13 seconds            dofroscra_ci-application-1
503e52fd9e68   mysql:8.0.28                     Up 13 seconds (healthy)  dofroscra_ci-mysql-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;(health: starting)&lt;/code&gt; and &lt;code&gt;(healthy)&lt;/code&gt; infos for the &lt;code&gt;mysql&lt;/code&gt; service.&lt;/p&gt;

&lt;p&gt;We can also get this info from &lt;code&gt;docker inspect&lt;/code&gt; (used by our  wait-for-service.sh script) via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker inspect --format "{{json .State.Health.Status }}" dofroscra_ci-mysql-1
"healthy"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI: We could also use the &lt;a href="https://docs.docker.com/compose/compose-file/#depends_on" rel="noopener noreferrer"&gt;&lt;code&gt;depends_on&lt;/code&gt; property&lt;/a&gt; with a  &lt;code&gt;condition: service_healthy&lt;/code&gt; on the &lt;code&gt;application&lt;/code&gt; container so that &lt;code&gt;docker compose&lt;/code&gt; would  only start the container once the &lt;code&gt;mysql&lt;/code&gt; service is healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this would "block" the &lt;code&gt;make docker-up&lt;/code&gt; until &lt;code&gt;mysql&lt;/code&gt; is actually up and running. In  our case this is not desirable, because we can do "other stuff" in the meantime (namely: run the  &lt;code&gt;qa&lt;/code&gt; checks, because they don't require a database) and thus save a couple of seconds on each CI  run.&lt;/p&gt;

&lt;p&gt;&lt;a id="build-target-ci"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Build target: &lt;code&gt;ci&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We've already introduced build targets in  &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#environments-and-build-targets" rel="noopener noreferrer"&gt;Environments and build targets&lt;/a&gt; and how to "choose" them &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env" rel="noopener noreferrer"&gt;through &lt;code&gt;make&lt;/code&gt; with the &lt;code&gt;ENV&lt;/code&gt; variable defined in a shared &lt;code&gt;.make/.env&lt;/code&gt; file&lt;/a&gt;. Short recap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a &lt;code&gt;.make/.env&lt;/code&gt; file via &lt;code&gt;make make-init&lt;/code&gt; that contains the &lt;code&gt;ENV&lt;/code&gt;, e.g.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;  &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;.make/.env&lt;/code&gt; file is included in the main &lt;code&gt;Makefile&lt;/code&gt;, making the &lt;code&gt;ENV&lt;/code&gt; variables available 
to &lt;code&gt;make&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#make-docker-3" rel="noopener noreferrer"&gt;configure a &lt;code&gt;$DOCKER_COMPOSE&lt;/code&gt; variable&lt;/a&gt; 
that passes the &lt;code&gt;ENV&lt;/code&gt; as an environment variable, i.e. via
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ENV&lt;span class="si"&gt;)&lt;/span&gt; docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fmake-init-ci-docker-commands.PNG" alt="Initialize make to run docker commands with ENV=ci"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the &lt;code&gt;ENV&lt;/code&gt; variable in the &lt;code&gt;docker compose&lt;/code&gt; configuration file to determine the 
&lt;code&gt;build.target&lt;/code&gt; property. E.g. in &lt;code&gt;.docker/docker-compose/docker-compose-php-base.yml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;php-base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ENV?}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/build-ci-images.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fbuild-ci-images.PNG" alt="Build images for CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in the &lt;code&gt;Dockerfile&lt;/code&gt; of a service, define the &lt;code&gt;ENV&lt;/code&gt; as a build stage. E.g. in 
&lt;code&gt;.docker/images/php/base/Dockerfile&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  FROM base as ci
  &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So to enable the new &lt;code&gt;ci&lt;/code&gt; environment, we need to modify the Dockerfiles for the &lt;code&gt;php-base&lt;/code&gt; and  the &lt;code&gt;application&lt;/code&gt; image.&lt;/p&gt;

&lt;p&gt;&lt;a id="build-stage-ci-in-the-php-base-image"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Build stage &lt;code&gt;ci&lt;/code&gt; in the &lt;code&gt;php-base&lt;/code&gt; image
&lt;/h4&gt;

&lt;p&gt;&lt;a id="use-the-whole-codebase-as-build-context"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Use the whole codebase as build context
&lt;/h5&gt;

&lt;p&gt;As mentioned in section Docker changes we want to "bake" the codebase into  the &lt;code&gt;ci&lt;/code&gt; image of the &lt;code&gt;php-base&lt;/code&gt; container. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fcodebase-in-docker-image.PNG" alt="Add the codebase in the docker image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thus, we must change the &lt;code&gt;context&lt;/code&gt; property in  &lt;code&gt;.docker/docker-compose/docker-compose-php-base.yml&lt;/code&gt; &lt;strong&gt;to not only use the &lt;code&gt;.docker/&lt;/code&gt; directory  but instead the whole codebase&lt;/strong&gt;. I.e. "dont use &lt;code&gt;../&lt;/code&gt; but &lt;code&gt;../../&lt;/code&gt;":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: .docker/docker-compose/docker-compose-php-base.yml&lt;/span&gt;

  &lt;span class="na"&gt;php-base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# pass the full codebase to docker for building the image&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../../&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="build-the-dependencies"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Build the dependencies
&lt;/h5&gt;

&lt;p&gt;The composer dependencies must be set up in the image as well, so we introduce a new stage  stage in &lt;code&gt;.docker/images/php/base/Dockerfile&lt;/code&gt;. The most trivial solution would look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;copy the whole codebase&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;composer install&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ci&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /codebase&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-scripts&lt;/span&gt; &lt;span class="nt"&gt;--no-plugins&lt;/span&gt; &lt;span class="nt"&gt;--no-progress&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this approach has some downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if &lt;em&gt;any&lt;/em&gt; file in the codebase changes, the &lt;code&gt;COPY . /codebase&lt;/code&gt; layer will be invalidated. I.e. 
docker could &lt;em&gt;not&lt;/em&gt; use the &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache" rel="noopener noreferrer"&gt;layer cache&lt;/a&gt;
which also means &lt;strong&gt;that every layer afterwards cannot use the cache&lt;/strong&gt; as well. In consequence the 
&lt;code&gt;composer install&lt;/code&gt; would run every time - even when the &lt;code&gt;composer.json&lt;/code&gt; file doesn't change.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://getcomposer.org/doc/06-config.md#cache-dir" rel="noopener noreferrer"&gt;&lt;code&gt;composer&lt;/code&gt; itself uses a cache&lt;/a&gt; for 
storing dependencies locally so it doesn't have to download dependencies that haven't changed.
But since we run &lt;code&gt;composer install&lt;/code&gt; &lt;em&gt;in Docker&lt;/em&gt;, this cache would be "thrown away" every time 
a build finishes. To mitigate that, we can use 
&lt;a href="https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#run---mounttypecache" rel="noopener noreferrer"&gt;&lt;code&gt;--mount=type=cache&lt;/code&gt;&lt;/a&gt;
to define a directory that docker will re-use between builds:
&amp;gt; Contents of the cache directories persists between builder invocations without invalidating 
&amp;gt; the instruction cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping those points in mind, we end up with the following instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: .docker/images/php/base/Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ci&lt;/span&gt;

&lt;span class="c"&gt;# By only copying the composer files required to run composer install&lt;/span&gt;
&lt;span class="c"&gt;# the layer will be cached and only invalidated when the composer dependencies are changed&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./composer.json /dependencies/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./composer.lock /dependencies/&lt;/span&gt;

&lt;span class="c"&gt;# use a cache mount to cache the composer dependencies&lt;/span&gt;
&lt;span class="c"&gt;# this is essentially a cache that lives in Docker BuildKit (i.e. has nothing to do with the host system) &lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/tmp/.composer &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;cd&lt;/span&gt; /dependencies &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="c"&gt;# COMPOSER_HOME=/tmp/.composer sets the home directory of composer that&lt;/span&gt;
    &lt;span class="c"&gt;# also controls where composer looks for the cache &lt;/span&gt;
    &lt;span class="c"&gt;# so we don't have to download dependencies again (if they are cached)&lt;/span&gt;
    COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o 

&lt;span class="c"&gt;# copy the full codebase&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /codebase&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; /dependencies/vendor /codebase/vendor &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;cd&lt;/span&gt; /codebase &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="c"&gt;# remove files we don't require in the image to keep the image size small&lt;/span&gt;
    rm -rf .docker/ &amp;amp;&amp;amp; \
    # we need a git repository for git-secret to work (can be an empty one)
    git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI: The &lt;code&gt;COPY . /codebase&lt;/code&gt; step doesn't actually copy "everything in the repository", because we  have also introduced a &lt;code&gt;.dockerignore&lt;/code&gt; file to exclude some files from being included in the  build context - see section &lt;code&gt;.dockerignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some notes on the final &lt;code&gt;RUN&lt;/code&gt; step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rm -rf .docker/&lt;/code&gt; doesn't really save "that much" in the current setup - please take it more 
as an example to remove any files that shouldn't end up in the final image (e.g. "tests in a 
production image")&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;git init&lt;/code&gt; part is required because we need to decrypt the secrets later - and 
&lt;code&gt;git-secret&lt;/code&gt; requires a &lt;code&gt;git&lt;/code&gt; repository (which can be empty). We can't decrypt the secrets 
during the build, because we do not want decrypted secret files to end up in the image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When tested locally, the difference between the trivial solution and the one that makes use of  layer caching is ~35 seconds, see the results in the Performance section.&lt;/p&gt;

&lt;p&gt;&lt;a id="create-the-final-image"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Create the final image
&lt;/h5&gt;

&lt;p&gt;As a final step, we will rename the current stage to &lt;code&gt;codebase&lt;/code&gt; and copy the "build  artifact" from that stage into our final &lt;code&gt;ci&lt;/code&gt; build stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;codebase&lt;/span&gt;

&lt;span class="c"&gt;# build the composer dependencies and clean up the copied files&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ci&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=codebase --chown=$APP_USER_NAME:$APP_GROUP_NAME /codebase $APP_CODE_PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why are we not just using the previous stage directly as &lt;code&gt;ci&lt;/code&gt;? &lt;/p&gt;

&lt;p&gt;Because using &lt;a href="https://docs.docker.com/develop/develop-images/multistage-build/" rel="noopener noreferrer"&gt;multistage-builds&lt;/a&gt;  is a  &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#use-multi-stage-builds" rel="noopener noreferrer"&gt;good practice to keep the final layers of an image to a minimum&lt;/a&gt;: Everything that "happened" in the previous &lt;code&gt;codebase&lt;/code&gt; stage will be "forgotten", i.e. not  exported as layers. &lt;/p&gt;

&lt;p&gt;That does not only save us some layers, but also allows us to get rid of files like the &lt;code&gt;.docker/&lt;/code&gt; directory. We needed that directory in the build context because some files where required in other parts of the &lt;code&gt;Dockerfile&lt;/code&gt; (e.g. the php ini files), so we can't exclude it via &lt;code&gt;.dockerignore&lt;/code&gt;. But we can remove it in the &lt;code&gt;codebase&lt;/code&gt; stage - so it will NOT be copied over and thus not end up in the final image. If we wouldn't have the &lt;code&gt;codebase&lt;/code&gt; stage, the folder would be part of the layer created when &lt;code&gt;COPY&lt;/code&gt;ing all the files from the build context and removing it via &lt;code&gt;rm -rf .docker/&lt;/code&gt; would have no effect on the image size.&lt;/p&gt;

&lt;p&gt;Currently, that doesn't really matter, because the building step is super simple (just a  &lt;code&gt;composer install&lt;/code&gt;) - but in a growing and more complex codebase you can easily save a couple MB.&lt;/p&gt;

&lt;p&gt;To be concrete, the &lt;strong&gt;multistage build has 31 layers&lt;/strong&gt; and the final layer containing the  codebase has a size of &lt;strong&gt;65.1MB&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker image history -H dofroscra/application-ci
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
d778c2ee8d5e   17 minutes ago   COPY /codebase /var/www/app # buildkit          65.1MB    buildkit.dockerfile.v0
                                                                                ^^^^^^
&amp;lt;missing&amp;gt;      17 minutes ago   WORKDIR /var/www/app                            0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   COPY /usr/bin/composer /usr/local/bin/compos…   2.36MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   COPY ./.docker/images/php/base/.bashrc /root…   395B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   COPY ./.docker/images/php/base/.bashrc /home…   395B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   196B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   378B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   1.28kB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      17 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   41MB      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ADD https://php.hernandev.com/key/php-alpine…   451B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   62.1MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ADD https://gitsecret.jfrog.io/artifactory/a…   450B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   4.74kB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV ENV=ci                                      0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV ALPINE_VERSION=3.15                         0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV TARGET_PHP_VERSION=8.1                      0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV APP_CODE_PATH=/var/www/app                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV APP_GROUP_NAME=application                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV APP_USER_NAME=application                   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV APP_GROUP_ID=10001                          0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ENV APP_USER_ID=10000                           0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG ENV                                         0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG ALPINE_VERSION                              0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG TARGET_PHP_VERSION                          0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG APP_CODE_PATH                               0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG APP_GROUP_NAME                              0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG APP_USER_NAME                               0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG APP_GROUP_ID                                0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      18 minutes ago   ARG APP_USER_ID                                 0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      2 days ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
&amp;lt;missing&amp;gt;      2 days ago       /bin/sh -c #(nop) ADD file:5d673d25da3a14ce1…   5.57MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;non-multistage build has 32 layers&lt;/strong&gt; and the final layer(s) containing the codebase have a combined size of &lt;strong&gt;65.15MB&lt;/strong&gt; (60.3MB + 4.85MB).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker image history -H dofroscra/application-ci
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
94ba50438c9a   2 minutes ago    RUN /bin/sh -c COMPOSER_HOME=/tmp/.composer …   60.3MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      2 minutes ago    COPY . /var/www/app # buildkit                  4.85MB    buildkit.dockerfile.v0
                                                                                ^^^^^^
&amp;lt;missing&amp;gt;      31 minutes ago   WORKDIR /var/www/app                            0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   COPY /usr/bin/composer /usr/local/bin/compos…   2.36MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   COPY ./.docker/images/php/base/.bashrc /root…   395B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   COPY ./.docker/images/php/base/.bashrc /home…   395B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   196B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   378B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   1.28kB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   41MB      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ADD https://php.hernandev.com/key/php-alpine…   451B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   62.1MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ADD https://gitsecret.jfrog.io/artifactory/a…   450B      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   4.74kB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV ENV=ci                                      0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV ALPINE_VERSION=3.15                         0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV TARGET_PHP_VERSION=8.1                      0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV APP_CODE_PATH=/var/www/app                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV APP_GROUP_NAME=application                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV APP_USER_NAME=application                   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV APP_GROUP_ID=10001                          0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ENV APP_USER_ID=10000                           0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG ENV                                         0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG ALPINE_VERSION                              0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG TARGET_PHP_VERSION                          0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG APP_CODE_PATH                               0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG APP_GROUP_NAME                              0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG APP_USER_NAME                               0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG APP_GROUP_ID                                0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      31 minutes ago   ARG APP_USER_ID                                 0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      2 days ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
&amp;lt;missing&amp;gt;      2 days ago       /bin/sh -c #(nop) ADD file:5d673d25da3a14ce1…   5.57MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again: It is expected that the differences aren't big, because the only size savings come from  the &lt;code&gt;.docker/&lt;/code&gt; directory with a size of ~70kb.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ du -hd 0 .docker
73K     .docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we are also using the &lt;a href="https://docs.docker.com/engine/reference/builder/#copy" rel="noopener noreferrer"&gt;&lt;code&gt;--chown&lt;/code&gt; option of the &lt;code&gt;RUN&lt;/code&gt; instruction&lt;/a&gt; to ensure that the files have the correct permissions.&lt;/p&gt;

&lt;p&gt;&lt;a id="build-stage-ci-in-the-application-image"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Build stage &lt;code&gt;ci&lt;/code&gt; in the &lt;code&gt;application&lt;/code&gt; image
&lt;/h4&gt;

&lt;p&gt;There is actually "nothing" to be done here. We don't need SSH any longer because it is only  required for the &lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/#ssh-configuration" rel="noopener noreferrer"&gt;SSH Configuration of PhpStorm&lt;/a&gt;. So the build stage is simply "empty":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BASE_IMAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;${BASE_IMAGE}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ci&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Though there is one thing to keep in mind: In the &lt;code&gt;local&lt;/code&gt; image we used &lt;code&gt;sshd&lt;/code&gt; as the entrypoint, i.e. we had a long running process that would keep the container running. To keep the  &lt;code&gt;ci&lt;/code&gt; application container running, we must&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start it via the &lt;code&gt;-d&lt;/code&gt; flag of &lt;code&gt;docker compose&lt;/code&gt; (already done in the &lt;code&gt;make docker-up&lt;/code&gt; target)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;  &lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;docker-up&lt;/span&gt;
  &lt;span class="nl"&gt;docker-up&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;validate-docker-variables&lt;/span&gt;
      &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com/a/55953120" rel="noopener noreferrer"&gt;allocate a &lt;code&gt;tty&lt;/code&gt; via &lt;code&gt;tty: true&lt;/code&gt;&lt;/a&gt; 
in the &lt;code&gt;docker-compose.local.ci.yml&lt;/code&gt; file
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="dockerignore"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  .dockerignore
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.docker.com/engine/reference/builder/#dockerignore-file" rel="noopener noreferrer"&gt;&lt;code&gt;.dockerignore&lt;/code&gt; file&lt;/a&gt;  is located in the root of the repository and ensures that certain files are kept out of the  Docker &lt;code&gt;build context&lt;/code&gt;. This will &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;speed up the build (because less files need to be transmitted to the docker daemon)&lt;/li&gt;
&lt;li&gt;keep images smaller (because irrelevant files are kept out of the image)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The syntax is quite similar to the &lt;code&gt;.gitignore&lt;/code&gt; file - in fact I've found it to be quite often  the case that the contents of the &lt;code&gt;.gitignore&lt;/code&gt; file are a subset of the &lt;code&gt;.dockerignore&lt;/code&gt; file. This  makes kinda sense, because you &lt;strong&gt;typically wouldn't want files that are excluded from the  repository to end up in a docker image&lt;/strong&gt; (e.g. unencrypted secret files). This has also been  noticed by others, see e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/docker/comments/evrfgp/any_way_to_copy_gitignore_contents_to_dockerignore/" rel="noopener noreferrer"&gt;Reddit: Any way to copy .gitignore contents to .dockerignore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/q/58707272/413531" rel="noopener noreferrer"&gt;SO: Should .dockerignore typically be a superset of .gitignore?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;but to my knowledge there is currently (2022-04-24) no way to "keep the two files in sync".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: The behavior between the two files is NOT identical! The documentation says&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Matching is done using Go’s filepath.Match rules. A preprocessing step removes leading and &lt;br&gt;
trailing whitespace and eliminates . and .. elements using Go’s filepath.Clean. Lines that are blank after preprocessing are ignored.&lt;/p&gt;

&lt;p&gt;Beyond Go’s filepath.Match rules, Docker also supports a special wildcard string ** that &lt;br&gt;
matches any number of directories (including zero). For example, **/*.go will exclude all &lt;br&gt;
files that end with .go that are found in all directories, including the root of the build context.&lt;/p&gt;

&lt;p&gt;Lines starting with ! (exclamation mark) can be used to make exceptions to exclusions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Please note the part regarding &lt;code&gt;**\*.go&lt;/code&gt;: In &lt;code&gt;.gitignore&lt;/code&gt; it would be sufficient to write  &lt;code&gt;.go&lt;/code&gt; to match &lt;em&gt;any&lt;/em&gt; file that contains &lt;code&gt;.go&lt;/code&gt;, regardless of the directory. In &lt;code&gt;.dockerignore&lt;/code&gt; you &lt;em&gt;must&lt;/em&gt; specify it as &lt;code&gt;**/*.go&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;In our case, the content of the &lt;code&gt;.dockerignore&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# gitignore
!.env.example
**/*.env .idea .phpunit.result.cache vendor/
secret.gpg .gitsecret/keys/random_seed .gitsecret/keys/pubring.kbx~
!*.secret passwords.txt .build

# additionally ignored files .git ```



&amp;lt;!-- generated --&amp;gt;
&amp;lt;a id='makefile-changes'&amp;gt; &amp;lt;/a&amp;gt;
&amp;lt;!-- /generated --&amp;gt;

## Makefile changes

&amp;lt;!-- generated --&amp;gt;
&amp;lt;a id='initialize-the-shared-variables'&amp;gt; &amp;lt;/a&amp;gt;
&amp;lt;!-- /generated --&amp;gt;

### Initialize the shared variables

We have introduced the concept of [shared variables via `.make/.env`](https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env)
previously. It allows us to **define variables in one place** (=single source 
of truth) that are then used as "defaults" so we **don't have to define them explicitly** when 
invoking certain `make` targets (like `make docker-build`). We'll make use of this concept by 
setting the environment to `ci`via`ENV=ci` and thus making sure that all docker commands use 
`ci` "automatically" as well.

[![Initialize make to run docker commands with ENV=ci](https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG)](https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG)

In addition, I made a small modification by **introducing a second file at `.make/variables.env`** 
that is also included in the main `Makefile` and **holds the "default" shared variables**. Those 
are neither "secret" nor are they likely to be changed for environment adjustments. The file 
is NOT ignored by `.gitignore` and is basically just the previous `.make/.env.example` file without 
the environment specific variables:



```text
# File .make/variables.env

DOCKER_REGISTRY=docker.io
DOCKER_NAMESPACE=dofroscra
APP_USER_NAME=application
APP_GROUP_NAME=application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.make/.env&lt;/code&gt; file is still &lt;code&gt;.gitignore&lt;/code&gt;d and can be initialized with the &lt;code&gt;make-init&lt;/code&gt; &lt;br&gt;
target using the &lt;code&gt;ENVS&lt;/code&gt; variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make make-init &lt;span class="nv"&gt;ENVS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=ci SOME_OTHER_DEFAULT_VARIABLE=foo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which would create a &lt;code&gt;.make/.env&lt;/code&gt; file with the content&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ENV=ci SOME_OTHER_DEFAULT_VARIABLE=foo&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;If necessary, we could also &lt;strong&gt;override variables defined in the &lt;code&gt;.make/variables.env&lt;/code&gt; file&lt;/strong&gt;,  because the &lt;code&gt;.make/.env&lt;/code&gt; is included last in the &lt;code&gt;Makefile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: Makefile
# ...
&lt;/span&gt;
&lt;span class="c"&gt;# include the default variables
&lt;/span&gt;&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="sx"&gt; .make/variables.env&lt;/span&gt;
&lt;span class="c"&gt;# include the local variables
&lt;/span&gt;&lt;span class="k"&gt;-include&lt;/span&gt;&lt;span class="sx"&gt; .make/.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default value for &lt;code&gt;ENVS&lt;/code&gt; is &lt;code&gt;ENV=local TAG=latest&lt;/code&gt; to retain the same default behavior as  before when &lt;code&gt;ENVS&lt;/code&gt; is omitted. The corresponding &lt;code&gt;make-init&lt;/code&gt; target is defined in the main  &lt;code&gt;Makefile&lt;/code&gt; and now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;ENVS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;latest
&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;make-init&lt;/span&gt;
&lt;span class="nl"&gt;make-init&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Initializes the local .makefile/.env file with ENV variables for make. Use via ENVS="KEY_1=value1 KEY_2=value2"&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;ENVS&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error ENVS is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .make/.env
    &lt;span class="k"&gt;for &lt;/span&gt;variable &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;ENVS&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt;variable | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; .make/.env &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="k"&gt;done&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Created a local .make/.env file"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="env-based-docker-compose-config"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ENV based &lt;code&gt;docker compose&lt;/code&gt; config
&lt;/h3&gt;

&lt;p&gt;As mentioned in section Compose file updates we need to select the  "correct" &lt;code&gt;docker compose&lt;/code&gt; configuration files based on the &lt;code&gt;ENV&lt;/code&gt; value. This is done in  &lt;code&gt;.make/02-00-docker.mk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# File .make/02-00-docker.mk
&lt;/span&gt;
&lt;span class="c"&gt;# ...
&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_COMPOSE_DIR&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;...
&lt;span class="nv"&gt;DOCKER_COMPOSE_COMMAND&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;...

&lt;span class="nv"&gt;DOCKER_COMPOSE_FILE_LOCAL_CI&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose.local.ci.yml
&lt;span class="nv"&gt;DOCKER_COMPOSE_FILE_CI&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose.ci.yml
&lt;span class="nv"&gt;DOCKER_COMPOSE_FILE_LOCAL&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose.local.yml

&lt;span class="c"&gt;# we need to "assemble" the correct combination of docker-compose.yml config files
&lt;/span&gt;&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(ENV),ci)&lt;/span&gt;
    &lt;span class="nv"&gt;DOCKER_COMPOSE_FILES&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE_LOCAL_CI&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE_CI&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;else&lt;/span&gt; &lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(ENV),local)&lt;/span&gt;
    &lt;span class="nv"&gt;DOCKER_COMPOSE_FILES&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE_LOCAL_CI&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE_LOCAL&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;

&lt;span class="nv"&gt;DOCKER_COMPOSE&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_COMMAND&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we now take a look at a full recipe when using &lt;code&gt;ENV=ci&lt;/code&gt; with a docker target (e.g.  &lt;code&gt;docker-up&lt;/code&gt;), we can see that the correct files are chosen, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make docker-up ENV=ci -n
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.yml -f ./.docker/docker-compose/docker-compose.ci.yml up -d

# =&amp;gt;
# -f ./.docker/docker-compose/docker-compose.local.ci.yml 
# -f ./.docker/docker-compose/docker-compose.ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/assemble-docker-compose-files.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fci-pipeline-docker-php-gitlab-github%2Fassemble-docker-compose-files.PNG" alt="Assemble docker-compose config files for CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="codebase-changes"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Codebase changes
&lt;/h2&gt;

&lt;p&gt;&lt;a id="add-a-test-for-encrypted-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="add-a-test-for-encrypted-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a test for encrypted files
&lt;/h3&gt;

&lt;p&gt;We've introduced &lt;code&gt;git-secret&lt;/code&gt; in the previous tutorial &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/" rel="noopener noreferrer"&gt;Use git-secret to encrypt secrets in the repository&lt;/a&gt;  and used it to store the file &lt;code&gt;passwords.txt&lt;/code&gt; encrypted in the codebase. To make sure that the decryption works as expected on the CI systems, I've added a test at &lt;code&gt;tests/Feature/EncryptionTest.php&lt;/code&gt; to check if the file exists and if the content is correct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EncryptionTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_ensure_that_the_secret_passwords_file_was_decrypted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$pathToSecretFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"/../../passwords.txt"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertFileExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pathToSecretFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my_secret_password&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$actual&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pathToSecretFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$actual&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course this doesn't make sense in a "real world scenario", because the secret value would now  be exposed in a test - but it suffices for now as proof of a working secret decryption.&lt;/p&gt;

&lt;p&gt;&lt;a id="add-a-password-protected-secret-gpg-key"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a password-protected secret &lt;code&gt;gpg&lt;/code&gt; key
&lt;/h3&gt;

&lt;p&gt;I've mentioned in &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#decrypt-files" rel="noopener noreferrer"&gt;Scenario: Decrypt file&lt;/a&gt; that it is also possible &lt;strong&gt;to use a password-protected secret &lt;code&gt;gpg&lt;/code&gt; key for an additional layer of security&lt;/strong&gt;. I have created such a key and stored it in the repository at &lt;code&gt;secret-protected.gpg.example&lt;/code&gt; (in a "real world scenario" I wouldn't do that - but since this is a public tutorial I want you to be able to follow along completely). The password for that key is &lt;code&gt;12345678&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The corresponding public key is located at &lt;code&gt;.dev/gpg-keys/alice-protected-public.gpg&lt;/code&gt; and belongs to the email address &lt;code&gt;alice.protected@example.com&lt;/code&gt;. I've  &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#adding-new-team-members" rel="noopener noreferrer"&gt;added this email address&lt;/a&gt; and &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#adding-and-encrypting-files" rel="noopener noreferrer"&gt;re-encrypted the secrets&lt;/a&gt; afterwards via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make gpg-init
make secret-add-user &lt;span class="nv"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alice.protected@example.com"&lt;/span&gt;
make secret-encrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I now import the &lt;code&gt;secret-protected.gpg.example&lt;/code&gt; key, I can decrypt the secrets, though I cannot use the usual &lt;code&gt;secret-decrypt&lt;/code&gt; target but must instead use &lt;code&gt;secret-decrypt-with-password&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-decrypt-with-password &lt;span class="nv"&gt;GPG_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12345678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or store the &lt;code&gt;GPG_PASSWORD&lt;/code&gt; in the &lt;code&gt;.make/.env&lt;/code&gt; file when it is initialized for CI&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make make-init &lt;span class="nv"&gt;ENVS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"&lt;/span&gt;
make secret-decrypt-with-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="create-a-junit-report-from-phpunit"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a JUnit report from PhpUnit
&lt;/h3&gt;

&lt;p&gt;I've added the &lt;a href="https://phpunit.readthedocs.io/en/9.5/textui.html?highlight=junit#command-line-options" rel="noopener noreferrer"&gt;&lt;code&gt;--log-junit&lt;/code&gt; option&lt;/a&gt; to the &lt;code&gt;phpunit&lt;/code&gt; configuration of the &lt;code&gt;test&lt;/code&gt; make target in order to create an XML report in the &lt;code&gt;.build/&lt;/code&gt; directory in the &lt;code&gt;.make/01-02-application-qa.mk&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: .make/01-02-application-qa.mk
# ...
&lt;/span&gt;
&lt;span class="nv"&gt;PHPUNIT_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpunit
&lt;span class="nv"&gt;PHPUNIT_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; phpunit.xml &lt;span class="nt"&gt;--log-junit&lt;/span&gt; .build/report.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I.e. each run of the tests will now create a &lt;a href="https://stackoverflow.com/questions/442556/spec-for-junit-xml-output" rel="noopener noreferrer"&gt;Junit XML report&lt;/a&gt; at &lt;code&gt;.build/report.xml&lt;/code&gt;. The file is used as an example of a build artifact, i.e. "something that we would like to keep" from a CI run.&lt;/p&gt;

&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. You should now have a working CI pipeline for Github (via Github Actions) and/or Gitlab (via Gitlab pipelines) that runs automatically on each push.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/" rel="noopener noreferrer"&gt;create a VM on GCP and provision it to run dockerized applications&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>php</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Setting up Git Bash / MINGW / MSYS2 on Windows</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Sat, 15 Apr 2023 12:25:38 +0000</pubDate>
      <link>https://dev.to/pascallandau/setting-up-git-bash-mingw-msys2-on-windows-24cc</link>
      <guid>https://dev.to/pascallandau/setting-up-git-bash-mingw-msys2-on-windows-24cc</guid>
      <description>&lt;p&gt;An installation instruction for Git Bash / MINGW / MSYS2 on Windows with some notes on solving common problems&lt;/p&gt;

&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/setting-up-git-bash-mingw-msys2-on-windows/" rel="noopener noreferrer"&gt;Setting up Git Bash / MINGW / MSYS2 on Windows&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In this article I'll document my process for &lt;strong&gt;setting up Git Bash / MINGW /  MSYS2 on Windows&lt;/strong&gt; including some additional configuration (e.g. installing &lt;code&gt;make&lt;/code&gt; and apply  some customizations via &lt;code&gt;.bashrc&lt;/code&gt;).&lt;/p&gt;



&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;
How to install and update Git Bash / MINGW / MSYS2 via Git for Windows

&lt;ul&gt;
&lt;li&gt;Update MINGW&lt;/li&gt;
&lt;li&gt;How to install &lt;code&gt;make&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Configuration via &lt;code&gt;.bashrc&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Common issues

&lt;ul&gt;
&lt;li&gt;The role of &lt;code&gt;winpty&lt;/code&gt;: Fixing "The input device is not a TTY" &lt;/li&gt;
&lt;li&gt;
The path conversion issue

&lt;ul&gt;
&lt;li&gt;Fixing the path conversion issue for MINGW / MSYS2&lt;/li&gt;
&lt;li&gt;Fixing the path conversion issue for &lt;code&gt;winpty&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Miscellaneous

&lt;ul&gt;
&lt;li&gt;Change the &lt;code&gt;bash&lt;/code&gt; custom prompt to a &lt;code&gt;$&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When I was learning &lt;code&gt;git&lt;/code&gt; I started with the fantastic  &lt;a href="https://gitforwindows.org/" rel="noopener noreferrer"&gt;Git for Windows&lt;/a&gt; package, that is maintained in the  &lt;a href="https://github.com/git-for-windows/" rel="noopener noreferrer"&gt;&lt;code&gt;git-for-windows/git&lt;/code&gt; Github repository&lt;/a&gt; and comes with  &lt;a href="https://www.atlassian.com/git/tutorials/git-bash" rel="noopener noreferrer"&gt;Git Bash&lt;/a&gt;, a shell that offers a  Unix-terminal like experience. It uses  &lt;a href="https://github.com/git-for-windows/git/wiki/The-difference-between-MINGW-and-MSYS2" rel="noopener noreferrer"&gt;MINGW and MSYS2 under the hood&lt;/a&gt; and does not only provide &lt;code&gt;git&lt;/code&gt; but also a bunch of other common Linux utilities like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash
sed
awk
ls
cp
rm
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I believe the main "shell" is actually powered by &lt;a href="https://www.mingw-w64.org/" rel="noopener noreferrer"&gt;MINGW64&lt;/a&gt; as  that's what will be shown by default:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/setting-up-git-bash-mingw-msys2-on-windows/mingw.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fsetting-up-git-bash-mingw-msys2-on-windows%2Fmingw.PNG" alt="Git Bash / MINGW shell"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thus, I will refer to the tool as MINGW shell or Git Bash throughout this article.&lt;/p&gt;

&lt;p&gt;I have been using MINGW for almost 10 years now, and it is still my go-to shell for Windows. I  could just never warm up to WSL, because the file sharing performance between WSL and native  Windows  files was (is?) horrible - but that's a different story.&lt;/p&gt;

&lt;p&gt;&lt;a id="how-to-install-and-update-git-bash-mingw-msys2-via-git-for-windows"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install and update Git Bash / MINGW / MSYS2 via Git for Windows
&lt;/h2&gt;

&lt;p&gt;You can find the latest Git for Windows installation package directly at the homepage of &lt;a href="https://gitforwindows.org/" rel="noopener noreferrer"&gt;https://gitforwindows.org/&lt;/a&gt;. Older releases can be found on  Github in the  &lt;a href="https://github.com/git-for-windows/git/releases" rel="noopener noreferrer"&gt;Releases section of the &lt;code&gt;git-for-windows/git&lt;/code&gt; repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow the instructions in the  &lt;a href="https://www.git-tower.com/blog/git-bash/#how-to-install-git-bash-on-windows" rel="noopener noreferrer"&gt;How to Install Git Bash on Windows article on git-tower.com&lt;/a&gt; to get a guided tour through the setup process. &lt;/p&gt;

&lt;p&gt;After the installation is finished, I usually create a desktop icon and assign the shortcut  &lt;code&gt;CTRL + ALT + B&lt;/code&gt; (for "&lt;strong&gt;b&lt;/strong&gt;ash") so that I can open a new shell session conveniently via keyboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/setting-up-git-bash-mingw-msys2-on-windows/git-bash-desktop-shortcut.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fsetting-up-git-bash-mingw-msys2-on-windows%2Fgit-bash-desktop-shortcut.PNG" alt="Git Bash desktop icon and shortcut"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="update-mingw"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Update MINGW
&lt;/h3&gt;

&lt;p&gt;To update Git for Windows, you can simply run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git update-git-for-windows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See also the &lt;a href="https://github.com/git-for-windows/git/wiki/FAQ#how-do-i-update-git-for-windows-upon-new-releases" rel="noopener noreferrer"&gt;Git for Windows FAQ under "How do I update Git for Windows upon new releases?"&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Git for Windows comes with a tool to check for updates and offer to install them. Whether or &lt;br&gt;
not you enabled auto-updates during installation, you can manually run &lt;br&gt;
&lt;code&gt;git update-git-for-windows&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can check the current version via &lt;code&gt;git version&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git --version
git version 2.37.2.windows.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




  
Your browser does not support the video tag.


&lt;p&gt;&lt;a id="how-to-install-make"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How to install &lt;code&gt;make&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;As per &lt;a href="https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make" rel="noopener noreferrer"&gt;How to add more to Git Bash on Windows: &lt;code&gt;make&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;a href="https://sourceforge.net/projects/ezwinports/files/" rel="noopener noreferrer"&gt;ezwinports&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Download file &lt;a href="https://sourceforge.net/projects/ezwinports/files/make-4.3-without-guile-w32-bin.zip/download" rel="noopener noreferrer"&gt;&lt;code&gt;make-4.3-without-guile-w32-bin.zip&lt;/code&gt;&lt;/a&gt;
(get the version without guile)&lt;/li&gt;
&lt;li&gt;Extract zip&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Copy the contents to your &lt;code&gt;Git/mingw64/&lt;/code&gt; directory, merging the folders, but do NOT&lt;br&gt;
overwrite/replace any existing files&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;navigate to the &lt;code&gt;Git/mingw64/&lt;/code&gt; directory via
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$(cd /; explorer .)
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Test via &lt;code&gt;make version&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make --version
GNU Make 4.3.1
Built for Windows32
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later &amp;lt;http://gnu.org/licenses/gpl.html&amp;gt;
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




  
Your browser does not support the video tag.


&lt;p&gt;PS: There's also an alternative way that I've outlined in &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#install-make-on-windows-mingw" rel="noopener noreferrer"&gt;Install &lt;code&gt;make&lt;/code&gt; on Windows (MinGW)&lt;/a&gt;, though the one explained here is easier/faster.&lt;/p&gt;

&lt;p&gt;&lt;a id="configuration-via-bashrc"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration via &lt;code&gt;.bashrc&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The MINGW shell is a &lt;code&gt;bash&lt;/code&gt; shell and can thus be  &lt;a href="https://www.digitalocean.com/community/tutorials/bashrc-file-in-linux" rel="noopener noreferrer"&gt;configured via a &lt;code&gt;.bashrc&lt;/code&gt; file&lt;/a&gt; located at  the home directory of the user. The shell supports the &lt;code&gt;~&lt;/code&gt; character as an alias for the home  directory, i.e. &lt;strong&gt;you can use &lt;code&gt;~/.bashrc&lt;/code&gt; to refer to the full path of the file&lt;/strong&gt;. This means you can  also edit it easily via &lt;code&gt;vi ~/.bashrc&lt;/code&gt; - though I prefer an actual GUI editor like &lt;a href="https://notepad-plus-plus.org/" rel="noopener noreferrer"&gt;Notepad++&lt;/a&gt;. A common workflow for me to open the file is  running the following commands in a MINGW shell session&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# navigate to to the home directory
cd ~

# open the file explorer
explorer .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;.bashrc&lt;/code&gt; file usually includes the following setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get bash completion for make targets by parsing make files in the current directory at &lt;/span&gt;
&lt;span class="c"&gt;#  the file "Makefile"&lt;/span&gt;
&lt;span class="c"&gt;#  all files with a ".mk" suffix in the folders ".make" and ".makefile"&lt;/span&gt;
&lt;span class="c"&gt;# see https://stackoverflow.com/questions/4188324/bash-completion-of-makefile-target&lt;/span&gt;
&lt;span class="c"&gt;# Notes:&lt;/span&gt;
&lt;span class="c"&gt;#  -h hides filenames&lt;/span&gt;
&lt;span class="c"&gt;#  -s hides error messages&lt;/span&gt;
&lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;grep -shoE '^[a-zA-Z0-9_.-]+:([^=]|&lt;/span&gt;&lt;span class="nv"&gt;$)&lt;/span&gt;&lt;span class="s2"&gt;' Makefile .make/*.mk .makefile/*.mk | sed 's/[^a-zA-Z0-9_.-]*&lt;/span&gt;&lt;span class="nv"&gt;$/&lt;/span&gt;&lt;span class="s2"&gt;/' | grep -v PHONY&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; make

&lt;span class="c"&gt;# Docker login helper&lt;/span&gt;
&lt;span class="c"&gt;# see https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via-din-bashrc-helper&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;din&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

  &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--user &lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bash"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;expr &lt;/span&gt;substr &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; 1 5&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"MINGW"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"winpty"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
  &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;prefix&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;filter&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;shell&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/4188324/bash-completion-of-makefile-target" rel="noopener noreferrer"&gt;SO: bash completion of makefile target&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via-din-bashrc-helper" rel="noopener noreferrer"&gt;Easy container access via din .bashrc helper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;a id="common-issues"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Common issues
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues" rel="noopener noreferrer"&gt;Git for Windows Known Issues page&lt;/a&gt; lists common problems with Git Bash and I want to provide some more context (and solutions)  to the things that I have encountered.&lt;/p&gt;



&lt;p&gt;&lt;a id="the-role-of-winpty-fixing-the-input-device-is-not-a-tty"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  The role of &lt;code&gt;winpty&lt;/code&gt;: Fixing "The input device is not a TTY"
&lt;/h3&gt;

&lt;p&gt;I encountered the &lt;code&gt;The input device is not a TTY&lt;/code&gt; error while using &lt;code&gt;docker&lt;/code&gt;. To log into a  running &lt;code&gt;docker&lt;/code&gt; container or starting a container with a login session, the &lt;a href="https://docs.docker.com/engine/reference/run/#foreground" rel="noopener noreferrer"&gt;&lt;code&gt;-i&lt;/code&gt; (Keep STDIN open even if not attached) and &lt;code&gt;-t&lt;/code&gt; (Allocate a pseudo-tty) options must be given:&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For interactive processes (like a shell), you must use &lt;code&gt;-i&lt;/code&gt; &lt;code&gt;-t&lt;/code&gt; together in order to allocate a&lt;br&gt;
tty for the container process. &lt;code&gt;-i&lt;/code&gt; &lt;code&gt;-t&lt;/code&gt;  is often written &lt;code&gt;-it&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But attempting to do so via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; busybox sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;yields the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run --rm -it busybox sh
the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fortunately, the fix is included in the message: Prefix the command with &lt;code&gt;winpty&lt;/code&gt;. Doing so works as expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty docker run --rm -it busybox sh
/ #
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/rprichard/winpty" rel="noopener noreferrer"&gt;&lt;code&gt;winpty&lt;/code&gt;&lt;/a&gt; is according to it's readme&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[...] a Windows software package providing an interface similar to a Unix pty-master for&lt;br&gt;
communicating with Windows console programs. The package consists of a library (libwinpty) and&lt;br&gt;
a tool for Cygwin and MSYS for running Windows console programs in a Cygwin/MSYS pty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So kind of a translator between your "Windows input" and the "command input" to create input  that is  &lt;a href="https://iximiuz.com/en/posts/linux-pty-what-powers-docker-attach-functionality/" rel="noopener noreferrer"&gt;compatible with a Unix pty (pty=pseudoterminal interface), e.g. for &lt;code&gt;docker&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues" rel="noopener noreferrer"&gt;Git for Windows Known Issues page&lt;/a&gt;, there are a number of other cases where &lt;code&gt;winpty&lt;/code&gt; is required (though I personally didn't encounter them yet):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Some console programs, most notably non-MSYS2 Python, PHP, Node and OpenSSL, interact&lt;br&gt;
correctly with MinTTY only when called through &lt;code&gt;winpty&lt;/code&gt; (e.g. the Python console needs to be&lt;br&gt;
started as &lt;code&gt;winpty python&lt;/code&gt; instead of just &lt;code&gt;python&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: I've seen people put an alias in their &lt;code&gt;.bashrc&lt;/code&gt; file to &lt;em&gt;always&lt;/em&gt; prefix &lt;code&gt;docker&lt;/code&gt;  commands automatically with &lt;code&gt;winpty&lt;/code&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alias docker="winpty docker"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, &lt;strong&gt;&lt;a href="https://superuser.com/q/1011597/434918" rel="noopener noreferrer"&gt;&lt;code&gt;winpty&lt;/code&gt; seems to break piping&lt;/a&gt; and can lead to unexpected results&lt;/strong&gt; like the error &lt;code&gt;stdout is not a tty&lt;/code&gt;. See the following  example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run --rm busybox echo "foo" | cat
foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty docker run --rm busybox echo "foo" | cat
stdout is not a tty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might work around this by adding the &lt;a href="https://github.com/rprichard/winpty/issues/103" rel="noopener noreferrer"&gt;(undocumented) &lt;code&gt;-Xallow-non-tty&lt;/code&gt;&lt;/a&gt; flag like so&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty -Xallow-non-tty docker run --rm busybox echo "foo" | cat
foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this &lt;a href="https://github.com/rprichard/winpty/issues/103#issuecomment-285987050" rel="noopener noreferrer"&gt;doesn't seem to be a catch-all solution&lt;/a&gt; and I would recommend against using it as a default - or if you do, only use it when the &lt;code&gt;-it&lt;/code&gt;  flag is used &lt;a href="https://stackoverflow.com/a/61580520/413531" rel="noopener noreferrer"&gt;as proposed in this answer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="the-path-conversion-issue"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The path conversion issue
&lt;/h3&gt;

&lt;p&gt;Ah. This one has given me &lt;em&gt;lots&lt;/em&gt; of headaches over the years. MINGW, MSYS2 and &lt;code&gt;winpty&lt;/code&gt; use  automatic conversion of Unix paths to Windows paths, e.g. &lt;code&gt;/foo&lt;/code&gt; gets translated to something like &lt;code&gt;C:/Program Files/Git/foo&lt;/code&gt; where &lt;code&gt;C:/Program Files/Git/&lt;/code&gt; is the installation directory of the  Git for Windows installation.&lt;/p&gt;

&lt;p&gt;&lt;a id="fixing-the-path-conversion-issue-for-mingw-msys2"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Fixing the path conversion issue for MINGW / MSYS2
&lt;/h4&gt;

&lt;p&gt;First, the behavior is mentioned on the &lt;a href="https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues" rel="noopener noreferrer"&gt;Git for Windows Known Issues page&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you specify command-line options starting with a slash, POSIX-to-Windows path conversion &lt;br&gt;
will kick in converting e.g. "&lt;code&gt;/usr/bin/bash.exe&lt;/code&gt;" to "&lt;code&gt;C:\Program Files\Git\usr\bin\bash.exe&lt;/code&gt;". &lt;br&gt;
When that is not desired -- e.g. "&lt;code&gt;--upload-pack=/opt/git/bin/git-upload-pack&lt;/code&gt;" or "&lt;code&gt;-L/regex/&lt;/code&gt;" &lt;br&gt;
-- you need to set the environment variable &lt;code&gt;MSYS_NO_PATHCONV&lt;/code&gt; temporarily, like so:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MSYS_NO_PATHCONV=1 git blame -L/pathconv/ msys2_path_conv.cc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Alternatively, you can double the first slash to avoid POSIX-to-Windows path conversion, e.g. &lt;br&gt;
"&lt;code&gt;//usr/bin/bash.exe&lt;/code&gt;".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and also documented for &lt;a href="https://web.archive.org/web/20201112005258/http://www.mingw.org/wiki/Posix_path_conversion" rel="noopener noreferrer"&gt;MINGW at "Posix path conversion"&lt;/a&gt;, but it's still brought up regularly, see e.g. &lt;a href="https://github.com/git-for-windows/git/issues/3619" rel="noopener noreferrer"&gt;GH #3619: "/" is replaced with the directory path of Git installation when using MinGW64 Bash&lt;/a&gt;. or &lt;a href="https://stackoverflow.com/a/34386471" rel="noopener noreferrer"&gt;SO: How to stop MinGW and MSYS from mangling path names given at the command line&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; busybox &lt;span class="nb"&gt;ls&lt;/span&gt; /foo
&lt;span class="nb"&gt;ls&lt;/span&gt;: C:/Program Files/Git/foo: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As quoted above, it can be solved by either&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding an &lt;strong&gt;additional &lt;code&gt;/&lt;/code&gt;&lt;/strong&gt; to the path
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ docker run --rm busybox ls //foo
  ls: /foo: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prefixing&lt;/strong&gt; the command with &lt;code&gt;MSYS_NO_PATHCONV=1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ MSYS_NO_PATHCONV=1 docker run --rm busybox ls /foo
  ls: /foo: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;or exporting the &lt;code&gt;MSYS_NO_PATHCONV=1&lt;/code&gt; variable as an &lt;strong&gt;environment variable&lt;/strong&gt; to disable the 
behavior completely
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ export MSYS_NO_PATHCONV=1
  $ docker run --rm busybox ls /foo
  ls: /foo: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CAUTION:&lt;/strong&gt; The value of the &lt;code&gt;MSYS_NO_PATHCONV&lt;/code&gt; variable does not matter - we can also set it  to &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; or an empty string. It only matters that the variable is defined!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ MSYS_NO_PATHCONV=0 docker run --rm busybox ls /foo
ls: /foo: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is particularly important when using the &lt;strong&gt;environment variable&lt;/strong&gt; approach. In order to  selectively enable the path conversion again, you must  &lt;a href="https://stackoverflow.com/a/41749660" rel="noopener noreferrer"&gt;unset the &lt;code&gt;MSYS_NO_PATHCONV&lt;/code&gt; first&lt;/a&gt; via  &lt;code&gt;env -u MSYS_NO_PATHCONV ...&lt;/code&gt;, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ env -u MSYS_NO_PATHCONV docker run --rm busybox ls /foo
ls: C:/Program Files/Git/foo: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




  
Your browser does not support the video tag.


&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: I've seen people adding &lt;code&gt;MSYS_NO_PATHCONV=1&lt;/code&gt; permanently to their environment in their &lt;code&gt;.bashrc&lt;/code&gt; file to &lt;em&gt;always&lt;/em&gt; disable path conversion via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MSYS_NO_PATHCONV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this can have some unintended side effects. When I tried it out, &lt;a href="https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/#set-up-the-gcloud-cli-tool" rel="noopener noreferrer"&gt;my local installation of the &lt;code&gt;gcloud&lt;/code&gt; cli&lt;/a&gt; stopped working with the error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ MSYS_NO_PATHCONV=1 gcloud version
C:\Users\Pascal\AppData\Local\Programs\Python\Python39\python.exe: can't open file 'C:\c\Users\Pascal\AppData\Local\Google\Cloud SDK\google-cloud-sdk\lib\gcloud.py': [Errno 2] No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead I recommend setting &lt;code&gt;MSYS_NO_PATHCONV=1&lt;/code&gt; either selectively per command or scope it to the use case. I do this for example in my Makefiles by only exporting it for the scope of &lt;code&gt;make&lt;/code&gt; (and all scripts &lt;code&gt;make&lt;/code&gt; invokes) by putting the following code in the beginning of the  Makefile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# OS is a defined variable for WIN systems, so "uname" will not be executed
&lt;/span&gt;&lt;span class="nv"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="nb"&gt;uname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# Values of OS:
#   Windows =&amp;gt; Windows_NT 
#   Mac     =&amp;gt; Darwin 
#   Linux   =&amp;gt; Linux 
&lt;/span&gt;&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(OS),Windows_NT)&lt;/span&gt;
    &lt;span class="k"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MSYS_NO_PATHCONV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path conversion is also documented for &lt;a href="https://www.msys2.org/docs/filesystem-paths/#automatic-unix-windows-path-conversion" rel="noopener noreferrer"&gt;MSYS2 at "Filesystem Paths: Automatic Unix ⟶ Windows Path Conversion"&lt;/a&gt; and can be disabled via the &lt;code&gt;MSYS2_ARG_CONV_EXCL&lt;/code&gt; environment variable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[...] For these cases you can exclude certain arguments via the &lt;code&gt;MSYS2_ARG_CONV_EXCL&lt;/code&gt; environment &lt;br&gt;
variable:&lt;br&gt;
[...]&lt;br&gt;
&lt;code&gt;MSYS2_ARG_CONV_EXCL&lt;/code&gt; can either be * to mean exclude everything, or a list of one ore more &lt;br&gt;
arguments prefixes separated by ;, like &lt;code&gt;MSYS2_ARG_CONV_EXCL=--dir=;--bla=;/test&lt;/code&gt;. It matches &lt;br&gt;
the prefix against the whole argument string.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I.e. setting the variable as &lt;code&gt;MSYS2_ARG_CONV_EXCL="*"&lt;/code&gt; should disable the path conversion  completely. I myself have never had to use this, though. Using &lt;code&gt;MSYS_NO_PATHCONV&lt;/code&gt; was always  sufficient.&lt;/p&gt;

&lt;p&gt;&lt;a id="fixing-the-path-conversion-issue-for-winpty"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Fixing the path conversion issue for &lt;code&gt;winpty&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Unfortunately, &lt;code&gt;winpty&lt;/code&gt; suffers from this path conversion issue as well. In the standard  installation of Git for Windows we can even see this by simply using &lt;code&gt;echo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty echo /
C:/Program Files/Git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is known and flagged as a bug e.g. in &lt;a href="https://github.com/msys2/MSYS2-packages/issues/411" rel="noopener noreferrer"&gt;GH issue #411: Path conversion with and without winpty differs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Remember the example I gave in section The role of &lt;code&gt;winpty&lt;/code&gt; e.g. when using &lt;code&gt;docker&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty docker run --rm -it busybox sh
/ #
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's extend this and throw a volume into the mix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty docker run --rm -v foo:/foo -it busybox sh
docker: Error response from daemon: create foo;C: "foo;C" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;winpty&lt;/code&gt; converts &lt;code&gt;/foo&lt;/code&gt; to &lt;code&gt;C:/Program Files/Git/foo&lt;/code&gt; so that the volume definition becomes &lt;code&gt;-v foo:C:/Program Files/Git/foo&lt;/code&gt; - which is of course invalid.&lt;/p&gt;

&lt;p&gt;Using an additional &lt;code&gt;/&lt;/code&gt; as a prefix does work here as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ winpty docker run --rm -v foo://foo -it busybox sh
/ #
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;strong&gt;there is no environment variable that we could use&lt;/strong&gt;. The only way to "fix" the path  conversion is using a  &lt;a href="https://github.com/rprichard/winpty/releases" rel="noopener noreferrer"&gt;newer release of &lt;code&gt;winpty&lt;/code&gt;&lt;/a&gt; and replace the one  that is shipped together with Git for Windows  &lt;a href="https://github.com/msys2/MSYS2-packages/issues/411#issuecomment-372585320" rel="noopener noreferrer"&gt;as proposed by the maintainer of &lt;code&gt;winpty&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://github.com/rprichard/winpty/issues/127#issuecomment-817356202" rel="noopener noreferrer"&gt;comment outlines the full process&lt;/a&gt; to replace &lt;code&gt;winpty&lt;/code&gt; and is (slightly adapted) as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# create temporary directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;temp
&lt;span class="nb"&gt;cd &lt;/span&gt;temp
&lt;span class="c"&gt;# download a newer release&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://github.com/rprichard/winpty/releases/download/0.4.3/winpty-0.4.3-msys2-2.7.0-x64.tar.gz &lt;span class="nt"&gt;--output&lt;/span&gt; winpty.tar.gz
&lt;span class="c"&gt;# extract the archive&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xvf&lt;/span&gt; winpty.tar.gz
&lt;span class="c"&gt;# copy the content of the bin/ folder to `/usr/bin` &lt;/span&gt;
&lt;span class="c"&gt;# (which resolves to e.g `C:/Program Files/Git/usr/bin`; replaces any existing files)&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;winpty-0.4.3-msys2-2.7.0-x64/bin/&lt;span class="k"&gt;*&lt;/span&gt; /usr/bin
&lt;span class="c"&gt;# delete the temporary directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; temp/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




  
Your browser does not support the video tag.


&lt;p&gt;Once the new version is installed, the path conversion does not happen any longer (even without  specifying any environment variables).&lt;/p&gt;

&lt;p&gt;Related comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/for-win/issues/1588#issuecomment-594938988" rel="noopener noreferrer"&gt;https://github.com/docker/for-win/issues/1588#issuecomment-594938988&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/for-win/issues/1588#issuecomment-698080757" rel="noopener noreferrer"&gt;https://github.com/docker/for-win/issues/1588#issuecomment-698080757&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Caution&lt;/strong&gt;&lt;br&gt;
After updating MinGW, the fix for &lt;code&gt;winpty&lt;/code&gt; is "gone"! &lt;/p&gt;


  
Your browser does not support the video tag.


&lt;p&gt;I.e. you need to re-run the steps above every time you run an update.&lt;/p&gt;



&lt;p&gt;&lt;a id="miscellaneous"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Miscellaneous
&lt;/h2&gt;

&lt;p&gt;Some stuff that I need from time to time - not necessarily only relevant for Git Bash.&lt;/p&gt;



&lt;p&gt;&lt;a id="change-the-bash-custom-prompt-to-a"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Change the &lt;code&gt;bash&lt;/code&gt; custom prompt to a &lt;code&gt;$&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Via &lt;code&gt;PS1=" $"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pascal@LAPTOP-0DNL2Q02 MINGW64 ~
$ PS1="$ "
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://www.cyberciti.biz/tips/howto-linux-unix-bash-shell-setup-prompt.html" rel="noopener noreferrer"&gt;How to Change / Set up bash custom prompt (PS1) in Linux&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mingw</category>
      <category>windows</category>
      <category>setup</category>
    </item>
    <item>
      <title>Use git-secret to encrypt secrets in the repository [Tutorial Part 6]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Mon, 04 Jul 2022 05:00:51 +0000</pubDate>
      <link>https://dev.to/pascallandau/use-git-secret-to-encrypt-secrets-in-the-repository-tutorial-part-6-53p5</link>
      <guid>https://dev.to/pascallandau/use-git-secret-to-encrypt-secrets-in-the-repository-tutorial-part-6-53p5</guid>
      <description>&lt;p&gt;How to use git-secret to encrypt secrets and store them in a git repository&lt;/p&gt;

&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/" rel="noopener noreferrer"&gt;Use git-secret to encrypt secrets in the repository [Tutorial Part 6]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In the sixth part of this tutorial series on developing PHP on Docker we will &lt;strong&gt;setup &lt;code&gt;git-secret&lt;/code&gt; to store secrets directly in the repository&lt;/strong&gt;. Everything will be handled through Docker and  added as make targets for a convenient workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/git-secret-encrypt-repository-docker/git-secret-example.gif" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fgit-secret-encrypt-repository-docker%2Fgit-secret-example.gif" title="git-secret example" alt="git-secret example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;br&gt;
FYI:  This tutorial is a precursor to the next a part &lt;br&gt;
&lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/" rel="noopener noreferrer"&gt;Create a CI pipeline for dockerized PHP Apps&lt;/a&gt;&lt;br&gt;
because dealing with secrets is an important aspect when setting up a CI system (and later when  deploying to production) - but I feel it's complex enough to warrant its own article.&lt;br&gt;
&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/pZ-vFMfKcLY"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.   You find the branch with the final result of this tutorial at &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-6-git-secret-encrypt-repository-docker" rel="noopener noreferrer"&gt;part-6-git-secret-encrypt-repository-docker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/php-qa-tools-make-docker/" rel="noopener noreferrer"&gt;Set up PHP QA tools and control them via make&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/" rel="noopener noreferrer"&gt;Create a CI pipeline for dockerized PHP Apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;
Tooling

&lt;ul&gt;
&lt;li&gt;
gpg

&lt;ul&gt;
&lt;li&gt;gpg installation&lt;/li&gt;
&lt;li&gt;
gpg usage

&lt;ul&gt;
&lt;li&gt;Create GPG key pair&lt;/li&gt;
&lt;li&gt;Export, list and import private GPG keys&lt;/li&gt;
&lt;li&gt;Export, list and import public GPG keys&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

git-secret

&lt;ul&gt;
&lt;li&gt;
git-secret installation

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;git&lt;/code&gt; permission issue&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

git-secret usage

&lt;ul&gt;
&lt;li&gt;
Initialize git-secret

&lt;ul&gt;
&lt;li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Adding, listing and removing users

&lt;ul&gt;
&lt;li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Adding, listing and removing files for encryption&lt;/li&gt;

&lt;li&gt;Encrypt files&lt;/li&gt;

&lt;li&gt;Decrypting files&lt;/li&gt;

&lt;li&gt;Show changes between encrypted and decrypted files&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Makefile adjustments&lt;/li&gt;

&lt;li&gt;

Workflow

&lt;ul&gt;
&lt;li&gt;
Process challenges

&lt;ul&gt;
&lt;li&gt;Updating secrets&lt;/li&gt;
&lt;li&gt;Code reviews and merge conflicts&lt;/li&gt;
&lt;li&gt;Local &lt;code&gt;git-secret&lt;/code&gt; and &lt;code&gt;gpg&lt;/code&gt; setup&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Scenarios

&lt;ul&gt;
&lt;li&gt;Initial setup of &lt;code&gt;gpg&lt;/code&gt; keys&lt;/li&gt;
&lt;li&gt;Initial setup of &lt;code&gt;git-secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Initialize &lt;code&gt;gpg&lt;/code&gt; after container startup&lt;/li&gt;
&lt;li&gt;Adding (new) team members&lt;/li&gt;
&lt;li&gt;Adding and encrypting files&lt;/li&gt;
&lt;li&gt;Decrypt files&lt;/li&gt;
&lt;li&gt;Removing files&lt;/li&gt;
&lt;li&gt;Removing team members&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

Pros and cons

&lt;ul&gt;
&lt;li&gt;Pro&lt;/li&gt;
&lt;li&gt;Cons&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Dealing with secrets (passwords, tokens, key files, etc.) is close to "naming things" when it comes to hard problems in software engineering. Some things to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;security is paramount&lt;/strong&gt; - but high security often goes hand in hand with high inconvenience

&lt;ul&gt;
&lt;li&gt;and if things get too complicated, people look for shortcuts...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;in a team, &lt;strong&gt;sharing certain secret values&lt;/strong&gt; is often mandatory

&lt;ul&gt;
&lt;li&gt;so now we need to think about secure ways to distribute and update secrets across multiple
people&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;concrete secret values often &lt;strong&gt;depend on the environment&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;inherently tricky to "test" or even "review", because those values are "by definition"
different on "your machine" than on "production"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In fact, entire products have been build around dealing with secrets, e.g. &lt;a href="https://www.vaultproject.io/" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt;, &lt;a href="https://aws.amazon.com/secrets-manager/" rel="noopener noreferrer"&gt;AWS Secrets Manager&lt;/a&gt; or the &lt;a href="https://cloud.google.com/secret-manager" rel="noopener noreferrer"&gt;GCP Secret Manager&lt;/a&gt;. Introducing those in a project comes with a certain overhead as it's yet another service that needs to be integrated and   maintained. Maybe it is the exactly right decision for your use-case - maybe it's overkill. By the end of this article you'll at least be aware of an alternative with a lower barrier to entry. See also the Pros and cons section in the end for an overview.&lt;/p&gt;

&lt;p&gt;Even though it's &lt;a href="https://withblue.ink/2021/05/07/storing-secrets-and-passwords-in-git-is-bad.html" rel="noopener noreferrer"&gt;generally not advised to store secrets in a repository&lt;/a&gt;, I'll propose exactly that in this tutorial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identify files that contain secret values&lt;/li&gt;
&lt;li&gt;make sure they are added to &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;encrypt them via &lt;code&gt;git-secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;commit the encrypted files to the repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, we will be able to call&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-decrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to reveal secrets in the codebase, make modifications to them if necessary and then run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-encrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to encrypt them again so that they can be committed (and pushed to the remote repository). To  see it in action, check out branch &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/git-secret-encrypt-repository-docker" rel="noopener noreferrer"&gt;part-6-git-secret-encrypt-repository-docker&lt;/a&gt; and run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# checkout the branch&lt;/span&gt;
git checkout part-6-git-secret-encrypt-repository-docker

&lt;span class="c"&gt;# build and start the docker setup&lt;/span&gt;
make make-init
make docker-build
make docker-up

&lt;span class="c"&gt;# "create" the secret key - the file "secret.gpg.example" would usually NOT live in the repo!&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;secret.gpg.example secret.gpg

&lt;span class="c"&gt;# initialize gpg&lt;/span&gt;
make gpg-init

&lt;span class="c"&gt;# ensure that the decrypted secret file does not exist&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;passwords.txt

&lt;span class="c"&gt;# decrypt the secret file&lt;/span&gt;
make secret-decrypt

&lt;span class="c"&gt;# show the content of the secret file&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;passwords.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="tooling"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling
&lt;/h2&gt;

&lt;p&gt;We will set up &lt;code&gt;gpg&lt;/code&gt; and &lt;code&gt;git-secret&lt;/code&gt; in the php &lt;code&gt;base&lt;/code&gt; image, so that the tools become available in all other containers. Please refer to &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;Docker from scratch for PHP 8.1 Applications in 2022&lt;/a&gt; for an in-depth explanation of the docker images.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;strong&amp;gt;Caution&amp;lt;/strong&amp;gt;


All following commands are 
&amp;lt;strong&amp;gt;executed &amp;lt;em&amp;gt;in&amp;lt;/em&amp;gt; the &amp;lt;code&amp;gt;application&amp;lt;/code&amp;gt; container.&amp;lt;/strong&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;strong&amp;gt;Tip:&amp;lt;/strong&amp;gt;
&amp;lt;br&amp;gt;
See &amp;lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via%20-din-bashrc-helper"&amp;gt;Easy container access via din .bashrc helper&amp;lt;/a&amp;gt;
for a convenient shortcut to log into docker containers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Please note, that there is a caveat when using &lt;code&gt;git-secret&lt;/code&gt; in a folder that is shared between  the host system and a docker container. I'll explain that in more detail (including a workaround)  in section  The &lt;code&gt;git-secret&lt;/code&gt; directory and the &lt;code&gt;gpg-agent&lt;/code&gt; socket.&lt;/p&gt;

&lt;p&gt;&lt;a id="gpg"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  gpg
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gpg&lt;/code&gt; is short for &lt;a href="https://gnupg.org/" rel="noopener noreferrer"&gt;The GNU Privacy Guard&lt;/a&gt; and is an open source implementation of the OpenPGP standard. In short, it allows us to create a personal key file pair (similar to SSH keys) with a private secret key and a public key that can be shared with other parties whose messages you want to decrypt.&lt;/p&gt;

&lt;p&gt;&lt;a id="gpg-installation"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  gpg installation
&lt;/h4&gt;

&lt;p&gt;To install it, we can simply run &lt;code&gt;apk add gnupg&lt;/code&gt; and thus update  &lt;code&gt;.docker/images/php/base/Dockerfile&lt;/code&gt; accordingly&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: .docker/images/php/base/Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        bash &lt;span class="se"&gt;\
&lt;/span&gt;        gnupg &lt;span class="se"&gt;\
&lt;/span&gt;        make &lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="c"&gt;#...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="gpg-usage"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  gpg usage
&lt;/h4&gt;

&lt;p&gt;I'll only cover the strictly necessary &lt;code&gt;gpg&lt;/code&gt; commands here. Please refer to &lt;a href="https://git-secret.io/#using-gpg" rel="noopener noreferrer"&gt;the "Using GPG" section in the &lt;code&gt;git-secret&lt;/code&gt; docu&lt;/a&gt; and/or &lt;a href="https://linuxhint.com/generate-gpg-keys-gpg/" rel="noopener noreferrer"&gt;How to generate PGP keys with GPG&lt;/a&gt; for further information.&lt;/p&gt;

&lt;p&gt;&lt;a id="create-gpg-key-pair"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Create GPG key pair
&lt;/h5&gt;

&lt;p&gt;We need &lt;code&gt;gpg&lt;/code&gt; to &lt;strong&gt;create the gpg key pair&lt;/strong&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Pascal Landau"&lt;/span&gt;
&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pascal.landau@example.com"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--batch&lt;/span&gt; &lt;span class="nt"&gt;--gen-key&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="sh"&gt;
Name-Email: &lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="sh"&gt;
Expire-Date: 0
%no-protection
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;%no-protection&lt;/code&gt; will create a key without password, see also &lt;a href="https://gist.github.com/woods/8970150" rel="noopener noreferrer"&gt;this gist to "Creating gpg keys non-interactively"&lt;/a&gt;. To use a password (e.g. &lt;code&gt;12345678&lt;/code&gt;, we could have replace the &lt;code&gt;%no-protection&lt;/code&gt; line with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Passphrase: 12345678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All options for the unattended creation are defined in the  &lt;a href="https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html" rel="noopener noreferrer"&gt;official docs at "Unattended key generation"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ name="Pascal Landau"
$ email="pascal.landau@example.com"
$ gpg --batch --gen-key &amp;lt;&amp;lt;EOF
&amp;gt; Key-Type: 1
&amp;gt; Key-Length: 2048
&amp;gt; Subkey-Type: 1
&amp;gt; Subkey-Length: 2048
&amp;gt; Name-Real: $name
&amp;gt; Name-Email: $email
&amp;gt; Expire-Date: 0
&amp;gt; %no-protection
&amp;gt; EOF
gpg: key E1E734E00B611C26 marked as ultimately trusted
gpg: revocation certificate stored as '/root/.gnupg/opengpg-revocs.d/74082D81525723F5BF5B2099E1E734E00B611C26.rev'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could also run &lt;code&gt;gpg --gen-key&lt;/code&gt; without the &lt;code&gt;--batch&lt;/code&gt; flag to be guided interactively through the process.&lt;/p&gt;

&lt;p&gt;&lt;a id="export-list-and-import-private-gpg-keys"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Export, list and import private GPG keys
&lt;/h5&gt;

&lt;p&gt;The &lt;strong&gt;private key can be exported&lt;/strong&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pascal.landau@example.com"&lt;/span&gt;
&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"secret.gpg"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;--export-secret-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This secret key must never be shared&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----BEGIN PGP PRIVATE KEY BLOCK-----

lQOYBF7VVBwBCADo9un+SySu/InHSkPDpFVKuZXg/s4BbZmqFtYjvUUSoRAeSejv
G21nwttQGut+F+GdpDJL6W4pmLS31Kxpt6LCAxhID+PRYiJQ4k3inJfeUx7Ws339
XDPO3Rys+CmnZchcEgnbOfQlEqo51DMj6mRF2Ra/6svh7lqhrixGx1BaKn6VlHkC
...
ncIcHxNZt7eK644nWDn7j52HsRi+wcWsZ9mjkUgZLtyMPJNB5qlKQ18QgVdEAhuZ
xT3SieoBPd+tZikhu3BqyIifmLnxOJOjOIhbQrgFiblvzU1iOUOTOcSIB+7A
=YmRm
-----END PGP PRIVATE KEY BLOCK-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All &lt;strong&gt;secret keys can be listed&lt;/strong&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpg &lt;span class="nt"&gt;--list-secret-keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --list-secret-keys
/root/.gnupg/pubring.kbx
------------------------
sec   rsa2048 2022-03-27 [SCEA]
      74082D81525723F5BF5B2099E1E734E00B611C26
uid           [ultimate] Pascal Landau &amp;lt;pascal.landau@example.com&amp;gt;
ssb   rsa2048 2022-03-27 [SEA]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can &lt;strong&gt;import the private key&lt;/strong&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"secret.gpg"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--import&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and get the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ path="secret.gpg"
$ gpg --import "$path"
gpg: key E1E734E00B611C26: "Pascal Landau &amp;lt;pascal.landau@example.com&amp;gt;" not changed
gpg: key E1E734E00B611C26: secret key imported
gpg: Total number processed: 1
gpg:              unchanged: 1
gpg:       secret keys read: 1
gpg:  secret keys unchanged: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt; If the secret key requires a password, you would now be prompted for it. We can  circumvent the prompt by using &lt;code&gt;--batch --yes --pinentry-mode loopback&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"secret.gpg"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--import&lt;/span&gt; &lt;span class="nt"&gt;--batch&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--pinentry-mode&lt;/span&gt; loopback &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See also &lt;a href="https://betakuang.medium.com/using-command-line-passphrase-input-for-gpg-with-git-for-windows-f78ae2c7cd2e" rel="noopener noreferrer"&gt;Using Command-Line Passphrase Input for GPG&lt;/a&gt;. In doing so, we don't need to provide the password just yet - but we must pass it later when we  attempt to decrypt files.&lt;/p&gt;

&lt;p&gt;&lt;a id="export-list-and-import-public-gpg-keys"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Export, list and import public GPG keys
&lt;/h5&gt;

&lt;p&gt;The &lt;strong&gt;public key can be exported&lt;/strong&gt; to &lt;code&gt;public.gpg&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pascal.landau@example.com"&lt;/span&gt;
&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"public.gpg"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;--export&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBF7VVBwBCADo9un+SySu/InHSkPDpFVKuZXg/s4BbZmqFtYjvUUSoRAeSejv
G21nwttQGut+F+GdpDJL6W4pmLS31Kxpt6LCAxhID+PRYiJQ4k3inJfeUx7Ws339
...
3LLbK7Qxz0cV12K7B+n2ei466QAYXo03a7WlsPWn0JTFCsHoCOphjaVsncIcHxNZ
t7eK644nWDn7j52HsRi+wcWsZ9mjkUgZLtyMPJNB5qlKQ18QgVdEAhuZxT3SieoB
Pd+tZikhu3BqyIifmLnxOJOjOIhbQrgFiblvzU1iOUOTOcSIB+7A
=g0hF
-----END PGP PUBLIC KEY BLOCK-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;List all public keys&lt;/strong&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpg &lt;span class="nt"&gt;--list-keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --list-keys
/root/.gnupg/pubring.kbx
------------------------
pub   rsa2048 2022-03-27 [SCEA]
      74082D81525723F5BF5B2099E1E734E00B611C26
uid           [ultimate] Pascal Landau &amp;lt;pascal.landau@example.com&amp;gt;
sub   rsa2048 2022-03-27 [SEA]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;public key can be imported&lt;/strong&gt; in the same way as private keys via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"public.gpg"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--import&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --import /var/www/app/public.gpg
gpg: key E1E734E00B611C26: "Pascal Landau &amp;lt;pascal.landau@example.com&amp;gt;" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="git-secret"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  git-secret
&lt;/h3&gt;

&lt;p&gt;The official website of &lt;a href="https://git-secret.io/" rel="noopener noreferrer"&gt;git-secret&lt;/a&gt; is already doing a great job of introducing the tool. In short, it allows us to &lt;strong&gt;declare certain files as "secrets"&lt;/strong&gt; and &lt;strong&gt;encrypt them via &lt;code&gt;gpg&lt;/code&gt;&lt;/strong&gt; - using the keys of all trusted parties. The encrypted file can then by &lt;strong&gt;stored safely directly in the git repository&lt;/strong&gt; and &lt;strong&gt;decrypted if required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial I'm using &lt;code&gt;git-secret v0.4.0&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret --version
0.4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="git-secret-installation"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  git-secret installation
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://git-secret.io/installation#alpine" rel="noopener noreferrer"&gt;installation instructions for Alpine&lt;/a&gt; read as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo 'https://gitsecret.jfrog.io/artifactory/git-secret-apk/all/main'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/apk/repositories
wget &lt;span class="nt"&gt;-O&lt;/span&gt; /etc/apk/keys/git-secret-apk.rsa.pub &lt;span class="s1"&gt;'https://gitsecret.jfrog.io/artifactory/api/security/keypair/public/repositories/git-secret-apk'&lt;/span&gt;
apk add &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; git-secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus, we need to account for a recent change in &lt;code&gt;git&lt;/code&gt; that requires that the parent directory is  owned by the user executing the &lt;code&gt;git&lt;/code&gt; command. See also the more detailed explanation in section The &lt;code&gt;git&lt;/code&gt; permission issue.&lt;/p&gt;

&lt;p&gt;We update the &lt;code&gt;.docker/images/php/base/Dockerfile&lt;/code&gt; accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: .docker/images/php/base/Dockerfile&lt;/span&gt;

&lt;span class="c"&gt;# install git-secret&lt;/span&gt;
&lt;span class="c"&gt;# @see https://git-secret.io/installation#alpine&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; https://gitsecret.jfrog.io/artifactory/api/security/keypair/public/repositories/git-secret-apk /etc/apk/keys/git-secret-apk.rsa.pub&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://gitsecret.jfrog.io/artifactory/git-secret-apk/all/main"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/apk/repositories  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apk add &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        bash &lt;span class="se"&gt;\
&lt;/span&gt;        git-secret &lt;span class="se"&gt;\
&lt;/span&gt;        gawk &lt;span class="se"&gt;\
&lt;/span&gt;        gnupg &lt;span class="se"&gt;\
&lt;/span&gt;        make &lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="c"&gt;#...&lt;/span&gt;

&lt;span class="c"&gt;# Fix the git permission issue&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;git config &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--add&lt;/span&gt; safe.directory &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_CODE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="the-git-permission-issue"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  The &lt;code&gt;git&lt;/code&gt; permission issue
&lt;/h5&gt;

&lt;p&gt;In April 2022,  &lt;a href="https://github.blog/2022-04-12-git-security-vulnerability-announced/" rel="noopener noreferrer"&gt;Github accounced the security vulnerability &lt;code&gt;CVE-2022-24765&lt;/code&gt;&lt;/a&gt;, that was fixed in &lt;code&gt;git v2.35.2&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This version changes Git’s behavior when looking for a top-level &lt;code&gt;.git&lt;/code&gt; directory to stop when &lt;br&gt;
its directory traversal changes ownership from the current user. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice, the following error occurs if the parent directory is not owned by the user that  executes the &lt;code&gt;git&lt;/code&gt; command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: fatal: unsafe repository ('/parent/dir/of/.git-folder' is owned by someone else)
To add an exception for this directory, call:

    git config --global --add safe.directory /parent/dir/of/.git-folder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using &lt;code&gt;git secret&lt;/code&gt;, we would get the slightly misleading error message&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git-secret: abort: not in dir with git repo. Use 'git init' or 'git clone', then in repo use 'git secret init'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can "fix" the issue by using the new multi-valued  &lt;a href="https://git-scm.com/docs/git-config/2.36.0#Documentation/git-config.txt-safedirectory" rel="noopener noreferrer"&gt;safe.directory&lt;/a&gt;  configuration via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git config --system --add safe.directory /parent/dir/of/.git-folder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, that we didn't use the suggested &lt;code&gt;--global&lt;/code&gt; option but &lt;code&gt;--system&lt;/code&gt; instead, so that the  configuration is set for &lt;em&gt;any&lt;/em&gt; user.&lt;/p&gt;

&lt;p&gt;Wait - why not just ensure &lt;strong&gt;that the parent directory of the &lt;code&gt;.git&lt;/code&gt; folder has the correct  permissions&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Well... there's currently (2022-05-28) a &lt;strong&gt;bug in Docker Desktop that makes the permissions of bind  mounts kinda unpredictable&lt;/strong&gt;, see  &lt;a href="https://github.com/docker/for-win/issues/12742" rel="noopener noreferrer"&gt;Ownership of files set via bind mount is set to user who accesses the file first&lt;/a&gt; and by applying the fix directly in the &lt;code&gt;Dockerfile&lt;/code&gt; we can solve the issue reliably.&lt;/p&gt;

&lt;p&gt;&lt;a id="git-secret-usage"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  git-secret usage
&lt;/h4&gt;

&lt;p&gt;&lt;a id="initialize-git-secret"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Initialize git-secret
&lt;/h5&gt;

&lt;p&gt;&lt;code&gt;git-secret&lt;/code&gt; is initialized via the following command &lt;em&gt;run in the root of the git repository&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret init
git-secret: init created: '/var/www/app/.gitsecret/'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We only need to do this once, because we'll commit the folder to git later. It contains the following files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git status | grep ".gitsecret"
        new file:   .gitsecret/keys/pubring.kbx
        new file:   .gitsecret/keys/pubring.kbx~
        new file:   .gitsecret/keys/trustdb.gpg
        new file:   .gitsecret/paths/mapping.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pubring.kbx~&lt;/code&gt; file (with the trailing tilde &lt;code&gt;~&lt;/code&gt;) is only a temporary file and can safely be git-ignored. See also &lt;a href="https://github.com/sobolevn/git-secret/issues/566#issuecomment-570059374" rel="noopener noreferrer"&gt;Can't find any docs about keyring.kbx~ file&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="the-git-secret-directory-and-the-gpg-agent-socket"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  The &lt;code&gt;git-secret&lt;/code&gt; directory and the &lt;code&gt;gpg-agent&lt;/code&gt; socket
&lt;/h6&gt;

&lt;p&gt;To use &lt;code&gt;git-secret&lt;/code&gt; in a directory that is &lt;strong&gt;shared between the host system and docker&lt;/strong&gt;, we need to  also run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tee&lt;/span&gt; .gitsecret/keys/S.gpg-agent &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
%Assuan%
socket=/tmp/S.gpg-agent
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;tee&lt;/span&gt; .gitsecret/keys/S.gpg-agent.ssh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
%Assuan%
socket=/tmp/S.gpg-agent.ssh
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;tee&lt;/span&gt; .gitsecret/keys/gpg-agent.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
extra-socket /tmp/S.gpg-agent.extra
browser-socket /tmp/S.gpg-agent.browser
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is necessary because there is an issue &lt;strong&gt;when &lt;code&gt;git-secret&lt;/code&gt; is used in a setup where the  codebase is shared between the host system and a docker container&lt;/strong&gt;.  I've explained the details in the Github issue &lt;a href="https://github.com/sobolevn/git-secret/issues/806" rel="noopener noreferrer"&gt;"gpg: can't connect to the agent: IPC connect call failed" error in docker alpine on shared volume&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gpg&lt;/code&gt; uses a &lt;code&gt;gpg-agent&lt;/code&gt; to perform its tasks and the two tools communicate through sockets 
that are created in the &lt;code&gt;--home-directory&lt;/code&gt; of the &lt;code&gt;gpg-agent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the agent is started implicitly through a &lt;code&gt;gpg&lt;/code&gt; command used by &lt;code&gt;git-secret&lt;/code&gt;, using the 
&lt;code&gt;.gitsecret/keys&lt;/code&gt; directories as a &lt;code&gt;--home-directory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;because the location of the &lt;code&gt;--home-directory&lt;/code&gt; is shared with the host system, the socket 
creation fails (potentially only an issue for Docker Desktop, see the related discussion in 
Github issue &lt;a href="https://github.com/docker/for-mac/issues/483#issuecomment-647325015" rel="noopener noreferrer"&gt;Support for sharing unix sockets&lt;/a&gt;)
The corresponding error messages are
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpg: can't connect to the agent: IPC connect call failed

gpg-agent: error binding socket to '/var/www/app/.gitsecret/keys/S.gpg-agent': I/O error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;workaround for this problem&lt;/strong&gt; can be found in  &lt;a href="https://askubuntu.com/a/1053594/1583296" rel="noopener noreferrer"&gt;this thread&lt;/a&gt;: Configure &lt;code&gt;gpg&lt;/code&gt; to use different  locations for the sockets by  &lt;a href="https://github.com/sobolevn/git-secret/issues/806#issuecomment-1084202671" rel="noopener noreferrer"&gt;placing additional &lt;code&gt;gpg&lt;/code&gt; configuration files in the &lt;code&gt;.gitsecret/keys&lt;/code&gt; directory&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S.gpg-agent&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%Assuan%
socket=/tmp/S.gpg-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;S.gpg-agent.ssh&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%Assuan%
socket=/tmp/S.gpg-agent.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;gpg-agent.conf&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extra-socket /tmp/S.gpg-agent.extra
browser-socket /tmp/S.gpg-agent.browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="adding-listing-and-removing-users"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Adding, listing and removing users
&lt;/h5&gt;

&lt;p&gt;To &lt;strong&gt;add a new user&lt;/strong&gt;, you must first import its public gpg key. Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pascal.landau@example.com"&lt;/span&gt;
git secret tell &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the user &lt;code&gt;pascal.landau@example.com&lt;/code&gt; will now be able to decrypt the secrets.&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;show the users&lt;/strong&gt; run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret whoknows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret whoknows
pascal.landau@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;To remove a user&lt;/strong&gt;, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pascal.landau@example.com"&lt;/span&gt;
git secret killperson &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI: This command was renamed to &lt;code&gt;removeperson&lt;/code&gt; in &lt;code&gt;git-secret &amp;gt;= 0.5.0&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret killperson pascal.landau@example.com
git-secret: removed keys.
git-secret: now [pascal.landau@example.com] do not have an access to the repository.
git-secret: make sure to hide the existing secrets again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User &lt;code&gt;pascal.landau@example.com&lt;/code&gt; will no longer be able to decrypt the secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caution: The secrets need to be re-encrypted after removing a user!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="reminder-rotate-the-encrypted-secrets"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Reminder: Rotate the encrypted secrets
&lt;/h6&gt;

&lt;p&gt;Please be aware that &lt;strong&gt;not only your secrets are stored in git, but who had access as well&lt;/strong&gt;. I.e.  even if you remove a user and re-encrypt the secrets, that user would &lt;strong&gt;still be able to decrypt  the secrets of a previous commit&lt;/strong&gt; (when the user was still added). In consequence, &lt;strong&gt;you need  to rotate the encrypted secrets themselves as well after removing a user&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But isn't that a great flaw in the system, making it a bad idea to use &lt;code&gt;git-secret&lt;/code&gt; in general? &lt;/p&gt;

&lt;p&gt;In my opinion: No. &lt;/p&gt;

&lt;p&gt;If the removed user had access to the secrets at &lt;strong&gt;any&lt;/strong&gt; point in time (no  matter where they have been stored), he could very well have just created a local copy or simply  "written them down". In terms of security there is really no "added downside" due to &lt;code&gt;git-secret&lt;/code&gt;. It just makes it &lt;em&gt;very&lt;/em&gt; clear that you &lt;em&gt;must&lt;/em&gt; rotate the secrets ¯\_(ツ)_/¯&lt;/p&gt;

&lt;p&gt;See also this  &lt;a href="https://news.ycombinator.com/item?id=11663403" rel="noopener noreferrer"&gt;lengthy discussion on &lt;code&gt;git-secret&lt;/code&gt; on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="adding-listing-and-removing-files-for-encryption"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Adding, listing and removing files for encryption
&lt;/h5&gt;

&lt;p&gt;Run &lt;code&gt;git secret add [filenames...]&lt;/code&gt; for &lt;strong&gt;files you want to encrypt&lt;/strong&gt;. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret add .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;.env&lt;/code&gt; is not added in &lt;code&gt;.gitignore&lt;/code&gt;, &lt;code&gt;git-secret&lt;/code&gt; will display a warning and add it  automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git-secret: these files are not in .gitignore: .env
git-secret: auto adding them to .env
git-secret: 1 item(s) added.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, the file is added with no warning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret add .env
git-secret: 1 item(s) added.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You only need to add files once. They are then stored in &lt;code&gt;.gitsecret/paths/mapping.cfg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat .gitsecret/paths/mapping.cfg
.env:505070fc20233cb426eac6a3414399d0f466710c993198b1088e897fdfbbb2d5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also show the added files via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret list
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caution: The files are not yet encrypted!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to &lt;strong&gt;remove a file from being encrypted&lt;/strong&gt;, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret remove .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret remove .env
git-secret: removed from index.
git-secret: ensure that files: [.env] are now not ignored.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="encrypt-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Encrypt files
&lt;/h5&gt;

&lt;p&gt;To actually &lt;strong&gt;encrypt the files&lt;/strong&gt;, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret hide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret hide
git-secret: done. 1 of 1 files are hidden.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The encrypted (binary) file is stored at &lt;code&gt;$filename.secret&lt;/code&gt;, i.e. &lt;code&gt;.env.secret&lt;/code&gt; in this case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat .env.secret
�☺♀♥�H~�B�Ӯ☺�"��▼♂F�►���l�Cs��S�@MHWs��e������{♣♫↕↓�L� ↕s�1�J$◄♥�;���ǆ֕�Za�����\u�ٲ&amp;amp; ¶��V�► ���6��
;&amp;lt;�d:��}ҨD%.�;��&amp;amp;��G����vWW�]&amp;gt;���߶��▲;D�+Rs�S→�Y!&amp;amp;J��۪8���ٔF��→f����*��$♠���&amp;amp;RC�8▼♂�☻z h��Z0M�T&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The encrypted files are de-cryptable &lt;strong&gt;for all users that have been added via &lt;code&gt;git secret tell&lt;/code&gt;&lt;/strong&gt;.  That also means that you need to &lt;strong&gt;run this command again whenever a new user is added&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="decrypting-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Decrypting files
&lt;/h5&gt;

&lt;p&gt;You can &lt;strong&gt;decrypt files&lt;/strong&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret reveal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git secret reveal
File '/var/www/app/.env' exists. Overwrite? (y/N) y
git-secret: done. 1 of 1 files are revealed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the files are decrypted and will overwrite the current, unencrypted files (if they already exist)

&lt;ul&gt;
&lt;li&gt;use the &lt;code&gt;-f&lt;/code&gt; option to force the overwrite and run non-interactively&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;if you only want to check the content of an encrypted file, you can use
&lt;code&gt;git secret cat $filename&lt;/code&gt; (e.g. &lt;code&gt;git secret cat .env&lt;/code&gt;)&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In case the secret &lt;code&gt;gpg&lt;/code&gt; key is password protected, you must pass the password  &lt;a href="https://git-secret.io/git-secret-reveal" rel="noopener noreferrer"&gt;via the &lt;code&gt;-p&lt;/code&gt; option&lt;/a&gt;. E.g. for password &lt;code&gt;123456&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret reveal &lt;span class="nt"&gt;-p&lt;/span&gt; 123456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="show-changes-between-encrypted-and-decrypted-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Show changes between encrypted and decrypted files
&lt;/h5&gt;

&lt;p&gt;One problem that comes with encrypted files: &lt;strong&gt;You can't review them during a code review in a remote tool&lt;/strong&gt;. So in order to understand what changes have been made, it is helpful to&lt;br&gt;
&lt;strong&gt;show the changes between the encrypted and the decrypted files&lt;/strong&gt;. This can be done via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git secret changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ echo "foo" &amp;gt;&amp;gt; .env
$ git secret changes
git-secret: changes in /var/www/app/.env:
--- /dev/fd/63
+++ /var/www/app/.env
@@ -34,3 +34,4 @@
 MAIL_ENCRYPTION=null
 MAIL_FROM_ADDRESS=null
 MAIL_FROM_NAME="${APP_NAME}"
+foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;+foo&lt;/code&gt; at the bottom of the output. It was added in the first line via  &lt;code&gt;echo "foo"&amp;gt; &amp;gt;&amp;gt; .env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="makefile-adjustments"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Makefile adjustments
&lt;/h2&gt;

&lt;p&gt;Since I won't be able to remember all the commands for &lt;code&gt;git-secret&lt;/code&gt; and &lt;code&gt;gpg&lt;/code&gt;, I've added them to the Makefile at &lt;code&gt;.make/01-00-application-setup.mk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: .make/01-00-application-setup.mk
&lt;/span&gt;
&lt;span class="c"&gt;#...
&lt;/span&gt;
&lt;span class="c"&gt;# gpg
&lt;/span&gt;
&lt;span class="nv"&gt;DEFAULT_SECRET_GPG_KEY&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;secret.gpg
&lt;span class="nv"&gt;DEFAULT_PUBLIC_GPG_KEYS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;.dev/gpg-keys/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg&lt;/span&gt;
&lt;span class="nl"&gt;gpg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run gpg commands. Specify the command e.g. via ARGS="--list-keys"&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; gpg &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-export-public-key&lt;/span&gt;
&lt;span class="nl"&gt;gpg-export-public-key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Export a gpg public key e.g. via EMAIL="john.doe@example.com" PATH=".dev/gpg-keys/john-public.gpg"&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PATH&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error PATH is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;EMAIL&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error EMAIL is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gpg --armor --export &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;PATH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-export-private-key&lt;/span&gt;
&lt;span class="nl"&gt;gpg-export-private-key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Export a gpg private key e.g. via EMAIL="john.doe@example.com" PATH="secret.gpg"&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PATH&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error PATH is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;EMAIL&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error EMAIL is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--output &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;PATH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; --armor --export-secret-key &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import&lt;/span&gt;
&lt;span class="nl"&gt;gpg-import&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Import a gpg key file e.g. via GPG_KEY_FILES="/path/to/file /path/to/file2"&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;GPG_KEY_FILES&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error GPG_KEY_FILES is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--import --batch --yes --pinentry-mode loopback &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;GPG_KEY_FILES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import-default-secret-key&lt;/span&gt;
&lt;span class="nl"&gt;gpg-import-default-secret-key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Import the default secret key&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg-import &lt;span class="nv"&gt;GPG_KEY_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;DEFAULT_SECRET_GPG_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import-default-public-keys&lt;/span&gt;
&lt;span class="nl"&gt;gpg-import-default-public-keys&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Import the default public keys&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg-import &lt;span class="nv"&gt;GPG_KEY_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;DEFAULT_PUBLIC_GPG_KEYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-init&lt;/span&gt;
&lt;span class="nl"&gt;gpg-init&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import-default-secret-key gpg-import-default-public-keys &lt;/span&gt;&lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Initialize gpg in the container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nf"&gt; i.e. import all public and private keys&lt;/span&gt;

&lt;span class="c"&gt;# git-secret
&lt;/span&gt;
&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;git-secret&lt;/span&gt;
&lt;span class="nl"&gt;git-secret&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run git-secret commands. Specify the command e.g. via ARGS="hide"&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; git-secret &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-init&lt;/span&gt;
&lt;span class="nl"&gt;secret-init&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Initialize git-secret in the repository via `git-secret init`&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"init"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-init-gpg-socket-config&lt;/span&gt;
&lt;span class="nl"&gt;secret-init-gpg-socket-config&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Initialize the config files to change the gpg socket locations&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"%Assuan%"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .gitsecret/keys/S.gpg-agent
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"socket=/tmp/S.gpg-agent"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitsecret/keys/S.gpg-agent
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"%Assuan%"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .gitsecret/keys/S.gpg-agent.ssh
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"socket=/tmp/S.gpg-agent.ssh"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitsecret/keys/S.gpg-agent.ssh
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"extra-socket /tmp/S.gpg-agent.extra"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .gitsecret/keys/gpg-agent.conf
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"browser-socket /tmp/S.gpg-agent.browser"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitsecret/keys/gpg-agent.conf

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-encrypt&lt;/span&gt;
&lt;span class="nl"&gt;secret-encrypt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Decrypt secret files via `git-secret hide`&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hide"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-decrypt&lt;/span&gt;
&lt;span class="nl"&gt;secret-decrypt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Decrypt secret files via `git-secret reveal -f`&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"reveal -f"&lt;/span&gt; 

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-decrypt-with-password&lt;/span&gt;
&lt;span class="nl"&gt;secret-decrypt-with-password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Decrypt secret files using a password for gpg via `git-secret reveal -f -p $(GPG_PASSWORD)`&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;GPG_PASSWORD&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error GPG_PASSWORD is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"reveal -f -p &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;GPG_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-add&lt;/span&gt;
&lt;span class="nl"&gt;secret-add&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Add a file to git secret via `git-secret add $FILE`&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;FILE&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error FILE is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"add &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-cat&lt;/span&gt;
&lt;span class="nl"&gt;secret-cat&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Show the contents of file to git secret via `git-secret cat $FILE`&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;FILE&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error FILE is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cat &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-list&lt;/span&gt;
&lt;span class="nl"&gt;secret-list&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; List all files added to git secret `git-secret list`&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"list"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-remove&lt;/span&gt;
&lt;span class="nl"&gt;secret-remove&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Remove a file from git secret via `git-secret remove $FILE`&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;FILE&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error FILE is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"remove &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-add-user&lt;/span&gt;
&lt;span class="nl"&gt;secret-add-user&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Remove a user from git secret via `git-secret tell $EMAIL`&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;EMAIL&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error EMAIL is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tell &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-show-users&lt;/span&gt;
&lt;span class="nl"&gt;secret-show-users&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Show all users that have access to git secret via `git-secret whoknows`&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"whoknows"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-remove-user&lt;/span&gt;
&lt;span class="nl"&gt;secret-remove-user&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Remove a user from git secret via `git-secret killperson $EMAIL`&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;EMAIL&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error EMAIL is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"killperson &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;secret-diff&lt;/span&gt;
&lt;span class="nl"&gt;secret-diff&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Show the diff between the content of encrypted and decrypted files via `git-secret changes`&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; git-secret &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"changes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="workflow"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow
&lt;/h2&gt;

&lt;p&gt;Working with &lt;code&gt;git-secret&lt;/code&gt; is pretty straight forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initialize &lt;code&gt;git-secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add all users&lt;/li&gt;
&lt;li&gt;add all secret files and make sure they are ignored via &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;encrypt the files&lt;/li&gt;
&lt;li&gt;commit the encrypted files like "any other file"&lt;/li&gt;
&lt;li&gt;if any changes were made by other team members to the files:

&lt;ul&gt;
&lt;li&gt;=&amp;gt; decrypt to get the most up-to-date ones&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;if any modifications are required from your side:

&lt;ul&gt;
&lt;li&gt;=&amp;gt; make the changes to the decrypted files and then re-encrypt them again&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;But: The devil is in the details. The Process challenges section explains some of the pitfalls that we have encountered and the Scenarios section gives some concrete examples for common scenarios.&lt;/p&gt;

&lt;p&gt;&lt;a id="process-challenges"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Process challenges
&lt;/h3&gt;

&lt;p&gt;From a process perspective we've encountered some challenges that I'd like to mention - including how we deal with them.&lt;/p&gt;

&lt;p&gt;&lt;a id="updating-secrets"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Updating secrets
&lt;/h4&gt;

&lt;p&gt;When updating secrets you must ensure to always &lt;strong&gt;decrypt the files first&lt;/strong&gt; in order to avoid using "stale" files that you might still have locally. I usually check out the latest &lt;code&gt;main&lt;/code&gt; branch and run &lt;code&gt;git secret reveal&lt;/code&gt; to have the most up-to-date versions of the secret files. You  could also use a &lt;a href="https://stackoverflow.com/a/4185449/413531" rel="noopener noreferrer"&gt;&lt;code&gt;post-merge&lt;/code&gt; git hook&lt;/a&gt; to do  this automatically, but I personally don't want to risk overwriting my local secret files by  accident.&lt;/p&gt;

&lt;p&gt;&lt;a id="code-reviews-and-merge-conflicts"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Code reviews and merge conflicts
&lt;/h4&gt;

&lt;p&gt;Since the &lt;strong&gt;encrypted files cannot be diffed meaningfully&lt;/strong&gt;, the code reviews become more difficult when secrets are involved. We use Gitlab for reviews and I usually first check the diff of the &lt;code&gt;.gitsecret/paths/mapping.cfg&lt;/code&gt; file to see "which files have changed" directly in the UI.&lt;/p&gt;

&lt;p&gt;In addition, I will&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkout the &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;decrypt the secrets via &lt;code&gt;git secret reveal -f&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;checkout the &lt;code&gt;feature-branch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;git secret changes&lt;/code&gt; to see the differences between the decrypted files from &lt;code&gt;main&lt;/code&gt; and the
encrypted files from &lt;code&gt;feature-branch&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things get even more complicated when multiple team members need to modify secret files at the same time on different branches, as &lt;strong&gt;the encrypted files cannot be compared - i.e. git cannot be smart  about delta updates&lt;/strong&gt;. The only way around this is coordinating the pull requests, i.e. merge the first, update the secrets of the second and then merge the second.&lt;/p&gt;

&lt;p&gt;Fortunately, this has only happened very rarely so far.&lt;/p&gt;

&lt;p&gt;&lt;a id="local-git-secret-and-gpg-setup"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Local &lt;code&gt;git-secret&lt;/code&gt; and &lt;code&gt;gpg&lt;/code&gt; setup
&lt;/h4&gt;

&lt;p&gt;Currently, all developers in our team have &lt;code&gt;git-secret&lt;/code&gt; installed locally (instead of using it through docker) and use their own &lt;code&gt;gpg&lt;/code&gt; keys.&lt;/p&gt;

&lt;p&gt;This means more onboarding overhead, because&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new dev must

&lt;ul&gt;
&lt;li&gt;install &lt;code&gt;git-secret&lt;/code&gt; locally (*)&lt;/li&gt;
&lt;li&gt;install and setup &lt;code&gt;gpg&lt;/code&gt; locally (*)&lt;/li&gt;
&lt;li&gt;create a &lt;code&gt;gpg&lt;/code&gt; key pair&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;the public key must be added by every other team member (*)&lt;/li&gt;

&lt;li&gt;the user of the key must be added via &lt;code&gt;git secret tell&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;the secrets must be re-encrypted&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And for offboarding&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the public key must be removed by every other team member (*)&lt;/li&gt;
&lt;li&gt;the user of the key must be removed via &lt;code&gt;git secret killperson&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the secrets must be re-encrypted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, we need to ensure that the &lt;code&gt;git-secret&lt;/code&gt; and &lt;code&gt;gpg&lt;/code&gt; versions are kept up-to-date for everyone to not run into any compatibility issues.&lt;/p&gt;

&lt;p&gt;As an alternative, I'm currently leaning more towards &lt;strong&gt;handling everything through docker&lt;/strong&gt; (as presented in this tutorial). All steps marked with (*) are then obsolete, i.e. there is no need  to setup &lt;code&gt;git-secret&lt;/code&gt; and &lt;code&gt;gpg&lt;/code&gt; locally. &lt;/p&gt;

&lt;p&gt;But the approach also comes with some downsides, because&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;secret key and all public keys have to be imported every time the container is started&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;each dev needs to put his private &lt;code&gt;gpg&lt;/code&gt; key "in the codebase"&lt;/strong&gt; (ignored by &lt;code&gt;.gitignore&lt;/code&gt;) so it 
can be shared with docker and imported by &lt;code&gt;gpg&lt;/code&gt; (in docker). The alternative would be using 
a single secret key that is   shared within the team - which feels very wrong :P&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To make this a little more convenient, &lt;strong&gt;we put the public gpg keys of every dev in the  repository&lt;/strong&gt; under &lt;code&gt;.dev/gpg-keys/&lt;/code&gt; and &lt;strong&gt;the private key has to be named &lt;code&gt;secret.gpg&lt;/code&gt; and put  in the root of the codebase&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;In this setup, &lt;code&gt;secret.gpg&lt;/code&gt; must also be added to the&lt;code&gt;.gitignore&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# File: .gitignore
#...
vendor/
secret.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The import can now be simplified with &lt;code&gt;make&lt;/code&gt; targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# gpg
&lt;/span&gt;
&lt;span class="nv"&gt;DEFAULT_SECRET_GPG_KEY&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;secret.gpg
&lt;span class="nv"&gt;DEFAULT_PUBLIC_GPG_KEYS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;.dev/gpg-keys/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg&lt;/span&gt;
&lt;span class="nl"&gt;gpg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run gpg commands. Specify the command e.g. via ARGS="--list-keys"&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; gpg &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import&lt;/span&gt;
&lt;span class="nl"&gt;gpg-import&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Import a gpg key file e.g. via GPG_KEY_FILES="/path/to/file /path/to/file2"&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;GPG_KEY_FILES&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error GPG_KEY_FILES is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--import --batch --yes --pinentry-mode loopback &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;GPG_KEY_FILES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import-default-secret-key&lt;/span&gt;
&lt;span class="nl"&gt;gpg-import-default-secret-key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Import the default secret key&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg-import &lt;span class="nv"&gt;GPG_KEY_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;DEFAULT_SECRET_GPG_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import-default-public-keys&lt;/span&gt;
&lt;span class="nl"&gt;gpg-import-default-public-keys&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Import the default public keys&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; gpg-import &lt;span class="nv"&gt;GPG_KEY_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;DEFAULT_PUBLIC_GPG_KEYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-init&lt;/span&gt;
&lt;span class="nl"&gt;gpg-init&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;gpg-import-default-secret-key gpg-import-default-public-keys &lt;/span&gt;&lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Initialize gpg in the container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nf"&gt; i.e. import all public and private keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Everything" can now be handled via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make gpg-init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that needs to be run one single time after a container has been started.&lt;/p&gt;

&lt;p&gt;&lt;a id="scenarios"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenarios
&lt;/h3&gt;

&lt;p&gt;The scenarios assume the following preconditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have checked out branch &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/git-secret-encrypt-repository-docker" rel="noopener noreferrer"&gt;part-6-git-secret-encrypt-repository-docker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  git checkout part-6-git-secret-encrypt-repository-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and no running docker containers&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  make docker-down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;You have deleted the existing &lt;code&gt;git-secret&lt;/code&gt; folder, the keys in &lt;code&gt;.dev/gpg-keys&lt;/code&gt;, the 
&lt;code&gt;secret.gpg&lt;/code&gt; key and the &lt;code&gt;passwords.*&lt;/code&gt; files
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .gitsecret/ .dev/gpg-keys/&lt;span class="k"&gt;*&lt;/span&gt; secret.gpg passwords.&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="initial-setup-of-gpg-keys"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Initial setup of &lt;code&gt;gpg&lt;/code&gt; keys
&lt;/h4&gt;

&lt;p&gt;Unfortunately, I didn't find a way to create and export &lt;code&gt;gpg&lt;/code&gt; keys through &lt;code&gt;make&lt;/code&gt; and &lt;code&gt;docker&lt;/code&gt;. You need to either run the commands interactively OR pass a string with newlines to it. Both things are horribly complicated with &lt;code&gt;make&lt;/code&gt; and docker. Thus, you need to log into the &lt;code&gt;application&lt;/code&gt; container and run the commands in there directly. Not great - but this needs to be done only once when a new developer is onboarded anyways.&lt;/p&gt;

&lt;p&gt;FYI: I usually log into containers via &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via-din-bashrc-helper" rel="noopener noreferrer"&gt;Easy container access via din .bashrc helper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The secret key is exported to &lt;code&gt;secret.gpg&lt;/code&gt; and the public key to &lt;code&gt;.dev/gpg-keys/alice-public.gpg&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# start the docker setup&lt;/span&gt;
make docker-up

&lt;span class="c"&gt;# log into the container ('winpty' is only required on Windows)&lt;/span&gt;
winpty docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-ti&lt;/span&gt; dofroscra_local-application-1 bash

&lt;span class="c"&gt;# export key pair&lt;/span&gt;
&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Alice Doe"&lt;/span&gt;
&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;
gpg &lt;span class="nt"&gt;--batch&lt;/span&gt; &lt;span class="nt"&gt;--gen-key&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="sh"&gt;
Name-Email: &lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="sh"&gt;
Expire-Date: 0
%no-protection
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# export the private key&lt;/span&gt;
gpg &lt;span class="nt"&gt;--output&lt;/span&gt; secret.gpg &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;--export-secret-key&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;

&lt;span class="c"&gt;# export the public key&lt;/span&gt;
gpg &lt;span class="nt"&gt;--armor&lt;/span&gt; &lt;span class="nt"&gt;--export&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .dev/gpg-keys/alice-public.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make docker-up
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml up -d
Container dofroscra_local-application-1  Created
...
Container dofroscra_local-application-1  Started
$ docker ps
CONTAINER ID   IMAGE                                COMMAND                  CREATED          STATUS          PORTS                NAMES
...
95f740607586   dofroscra/application-local:latest   "/usr/sbin/sshd -D"      21 minutes ago   Up 21 minutes   0.0.0.0:2222-&amp;gt;22/tcp dofroscra_local-application-1

$ winpty docker exec -ti dofroscra_local-application-1 bash
root:/var/www/app# name="Alice Doe"
root:/var/www/app# email="alice@example.com"
gpg --batch --gen-key &amp;lt;&amp;lt;EOF
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: $name
Name-Email: $email
Expire-Date: 0
%no-protection
EOF
root:/var/www/app# gpg --batch --gen-key &amp;lt;&amp;lt;EOF
&amp;gt; Key-Type: 1
&amp;gt; Key-Length: 2048
&amp;gt; Subkey-Type: 1
&amp;gt; Subkey-Length: 2048
&amp;gt; Name-Real: $name
&amp;gt; Name-Email: $email
&amp;gt; Expire-Date: 0
&amp;gt; %no-protection
&amp;gt; EOF
gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key BBBE654440E720C1 marked as ultimately trusted
gpg: directory '/root/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/225C736E0E70AC222C072B70BBBE654440E720C1.rev'

root:/var/www/app# gpg --output secret.gpg --armor --export-secret-key $email
root:/var/www/app# head secret.gpg
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQOYBGJD+bwBCADBGKySV5PINc5MmQB3PNvCG7Oa1VMBO8XJdivIOSw7ykv55PRP
3g3R+ERd1Ss5gd5KAxLc1tt6PHGSPTypUJjCng2plwD8Jy5A/cC6o2x8yubOslLa
x1EC9fpcxUYUNXZavtEr+ylOaTaRz6qwSabsAgkg2NZ0ey/QKmFOZvhL8NlK9lTI
GgZPTiqPCsr7hiNg0WRbT5h8nTmfpl/DdTgwfPsDn5Hn0TEMa79WsrPnnq16jsq0
Uusuw3tOmdSdYnT8j7m1cpgcSj0hRF1eh4GVE0o62GqeLTWW9mfpcuv7n6mWaCB8
DCH6H238gwUriq/aboegcuBktlvSY21q/MIXABEBAAEAB/wK/M2buX+vavRgDRgR
hjUrsJTXO3VGLYcIetYXRhLmHLxBriKtcBa8OxLKKL5AFEuNourOBdcmTPiEwuxH
5s39IQOTrK6B1UmUqXvFLasXghorv8o8KGRL4ABM4Bgn6o+KBAVLVIwvVIhQ4rlf

root:/var/www/app# gpg --armor --export $email &amp;gt; .dev/gpg-keys/alice-public.gpg
root:/var/www/app# head .dev/gpg-keys/alice-public.gpg
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBGJD+bwBCADBGKySV5PINc5MmQB3PNvCG7Oa1VMBO8XJdivIOSw7ykv55PRP
3g3R+ERd1Ss5gd5KAxLc1tt6PHGSPTypUJjCng2plwD8Jy5A/cC6o2x8yubOslLa
x1EC9fpcxUYUNXZavtEr+ylOaTaRz6qwSabsAgkg2NZ0ey/QKmFOZvhL8NlK9lTI
GgZPTiqPCsr7hiNg0WRbT5h8nTmfpl/DdTgwfPsDn5Hn0TEMa79WsrPnnq16jsq0
Uusuw3tOmdSdYnT8j7m1cpgcSj0hRF1eh4GVE0o62GqeLTWW9mfpcuv7n6mWaCB8
DCH6H238gwUriq/aboegcuBktlvSY21q/MIXABEBAAG0HUFsaWNlIERvZSA8YWxp
Y2VAZXhhbXBsZS5jb20+iQFOBBMBCgA4FiEEIlxzbg5wrCIsBytwu75lREDnIMEF
AmJD+bwCGy8FCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQu75lREDnIMEN4Af+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. We now have a new secret and private key for &lt;code&gt;alice@example.com&lt;/code&gt; and have exported it to  &lt;code&gt;secret.gpg&lt;/code&gt; resp. &lt;code&gt;.dev/gpg-keys/alice-public.gpg&lt;/code&gt; (and thus shared it with the host system).  The remaining commands can now be run outside of the &lt;code&gt;application&lt;/code&gt; container directly on the  host system.&lt;/p&gt;

&lt;p&gt;&lt;a id="initial-setup-of-git-secret"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Initial setup of &lt;code&gt;git-secret&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Let's say we want to introduce &lt;code&gt;git-secret&lt;/code&gt; "from scratch" to a new codebase. Then you would run  the following commands:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize &lt;code&gt;git-secret&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-init
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="init";
git-secret: init created: '/var/www/app/.gitsecret/'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Apply the &lt;code&gt;gpg&lt;/code&gt; fix for shared directories&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;See The &lt;code&gt;git-secret&lt;/code&gt; directory and the &lt;code&gt;gpg-agent&lt;/code&gt; socket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;make secret-init-gpg-socket-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-init-gpg-socket-config
echo "%Assuan%" &amp;gt; .gitsecret/keys/S.gpg-agent
echo "socket=/tmp/S.gpg-agent" &amp;gt;&amp;gt; .gitsecret/keys/S.gpg-agent
echo "%Assuan%" &amp;gt; .gitsecret/keys/S.gpg-agent.ssh
echo "socket=/tmp/S.gpg-agent.ssh" &amp;gt;&amp;gt; .gitsecret/keys/S.gpg-agent.ssh
echo "extra-socket /tmp/S.gpg-agent.extra" &amp;gt; .gitsecret/keys/gpg-agent.conf
echo "browser-socket /tmp/S.gpg-agent.browser" &amp;gt;&amp;gt; .gitsecret/keys/gpg-agent.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="initialize-gpg-after-container-startup"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Initialize &lt;code&gt;gpg&lt;/code&gt; after container startup
&lt;/h4&gt;

&lt;p&gt;After restarting the containers, we need to initialize &lt;code&gt;gpg&lt;/code&gt;, i.e. import all public keys from  &lt;code&gt;.dev/gpg-keys/*&lt;/code&gt; and the private key from &lt;code&gt;secret.gpg&lt;/code&gt;. Otherwise we will not be able to en-  and decrypt the files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make gpg-init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make gpg-init
"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES="secret.gpg"
gpg: directory '/home/application/.gnupg' created
gpg: keybox '/home/application/.gnupg/pubring.kbx' created
gpg: /home/application/.gnupg/trustdb.gpg: trustdb created
gpg: key BBBE654440E720C1: public key "Alice Doe &amp;lt;alice@example.com&amp;gt;" imported
gpg: key BBBE654440E720C1: secret key imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1
"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES=".dev/gpg-keys/*"
gpg: key BBBE654440E720C1: "Alice Doe &amp;lt;alice@example.com&amp;gt;" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="adding-new-team-members"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding (new) team members
&lt;/h4&gt;

&lt;p&gt;Let's start by adding our own user to &lt;code&gt;git-secret&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-add-user &lt;span class="nv"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-add-user EMAIL="alice@example.com"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="tell alice@example.com"
git-secret: done. alice@example.com added as user(s) who know the secret.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And verify that it worked via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-show-users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-show-users
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="whoknows"
alice@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="adding-and-encrypting-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding and encrypting files
&lt;/h4&gt;

&lt;p&gt;Let's add a new encrypted file &lt;code&gt;secret_password.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create the file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"my_new_secret_password"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; secret_password.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to &lt;code&gt;.gitignore&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"secret_password.txt"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to &lt;code&gt;git-secret&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-add &lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"secret_password.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-add FILE="secret_password.txt"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="add secret_password.txt"
git-secret: 1 item(s) added.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encrypt all files&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-encrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-encrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="hide"
git-secret: done. 1 of 1 files are hidden.

$ ls secret_password.txt.secret
secret_password.txt.secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="decrypt-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Decrypt files
&lt;/h4&gt;

&lt;p&gt;Let's first remove the "plain" &lt;code&gt;secret_password.txt&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm &lt;/span&gt;secret_password.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rm secret_password.txt

$ ls secret_password.txt
ls: cannot access 'secret_password.txt': No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then decrypt the encrypted one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-decrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-decrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f"
git-secret: done. 1 of 1 files are revealed.

$ cat secret_password.txt
my_new_secret_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt; If the secret &lt;code&gt;gpg&lt;/code&gt; key is password protected (e.g. &lt;code&gt;123456&lt;/code&gt;), run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-decrypt-with-password &lt;span class="nv"&gt;GPG_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;123456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could also add the &lt;code&gt;GPG_PASSWORD&lt;/code&gt; variable to the  &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env" rel="noopener noreferrer"&gt;&lt;code&gt;.make/.env&lt;/code&gt;&lt;/a&gt; file as a local default value so that you wouldn't have to specify the value every time and  could then simply run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-decrypt-with-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without passing &lt;code&gt;GPG_PASSWORD&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="removing-files"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Removing files
&lt;/h4&gt;

&lt;p&gt;Remove the &lt;code&gt;secret_password.txt&lt;/code&gt; file we added previously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-remove &lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"secret_password.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-remove FILE="secret_password.txt"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="remove secret_password.txt"
git-secret: removed from index.
git-secret: ensure that files: [secret_password.txt] are now not ignored.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caution: this will neither remove the &lt;code&gt;secret_password.txt&lt;/code&gt; file nor  the &lt;code&gt;secret_password.txt.secret&lt;/code&gt; file automatically"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ls -l | grep secret_password.txt
-rw-r--r-- 1 Pascal 197121     19 Mar 31 14:03 secret_password.txt
-rw-r--r-- 1 Pascal 197121    358 Mar 31 14:02 secret_password.txt.secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But even though the encrypted &lt;code&gt;secret_password.txt.secret&lt;/code&gt; file still exists, it will not be  decrypted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-decrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f"
git-secret: done. 0 of 0 files are revealed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="removing-team-members"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Removing team members
&lt;/h4&gt;

&lt;p&gt;Removing a team member can be done via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-remove-user &lt;span class="nv"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-remove-user EMAIL="alice@example.com"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="killperson alice@example.com"
git-secret: removed keys.
git-secret: now [alice@example.com] do not have an access to the repository.
git-secret: make sure to hide the existing secrets again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are any users left, we must make sure to re-encrypt the secrets via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make secret-encrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise (if no more users are left) &lt;code&gt;git-secret&lt;/code&gt; would simply error out&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make secret-decrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f"
git-secret: abort: no public keys for users found. run 'git secret tell email@address'.
make[1]: *** [.make/01-00-application-setup.mk:57: git-secret] Error 1
make: *** [.make/01-00-application-setup.mk:69: secret-decrypt] Error 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caution&lt;/strong&gt;: Please keep in mind to  rotate the secrets themselves as well!&lt;/p&gt;

&lt;p&gt;&lt;a id="pros-and-cons"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros and cons
&lt;/h2&gt;

&lt;p&gt;&lt;a id="pro"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pro
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;very low barrier to entry:

&lt;ul&gt;
&lt;li&gt;no third party service required&lt;/li&gt;
&lt;li&gt;easy to integrate in existing codebases, because the secrets are located directly in 
the codebase&lt;/li&gt;
&lt;li&gt;everything can be handled through docker (no additional local software necessary)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;once set up, it is very easy/convenient to use and can be integrated in a team workflow&lt;/li&gt;

&lt;li&gt;changes to secrets can be reviewed before they are merged

&lt;ul&gt;
&lt;li&gt;this leads to less fuck-ups on deployments&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;"everything" is in the repository, which brings a lot of familiar benefits like

&lt;ul&gt;
&lt;li&gt;version control&lt;/li&gt;
&lt;li&gt;a single &lt;code&gt;git pull&lt;/code&gt; is the only thing you need to get everything (=&amp;gt; good dev experience)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="cons"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;some overhead during onboarding and offboarding&lt;/li&gt;
&lt;li&gt;the secret key must be put in the root of the repository at &lt;code&gt;./secret.gpg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;no fine grained permissions for different secrets, e.g. the mysql password on production and 
staging can not be treated differently

&lt;ul&gt;
&lt;li&gt;if somebody can decrypt secrets, ALL of them are exposed&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;if the a secret key ever gets leaked, all secrets are compromised

&lt;ul&gt;
&lt;li&gt;=&amp;gt; can be mitigated (to a degree) by using a passphrase on the secret key&lt;/li&gt;
&lt;li&gt;=&amp;gt; this is kinda true for any other system that stores secrets as well BUT third parties 
could probably implement additional measures like multi factor authentication&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;secrets are versioned alongside the users that have access, i.e. even if a user is removed at 
some point, he can still decrypt a previous version of the encrypted secrets

&lt;ul&gt;
&lt;li&gt;=&amp;gt; must be mitigated by
rotating the secrets themselves as well
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. You are now able to encrypt and decrypt secret files so that they can be stored  directly in the git repository.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/" rel="noopener noreferrer"&gt;set up a CI pipeline for dockerized PHP Apps on Github and Gitlab&lt;/a&gt; that decrypts all necessary secrets and then runs our tests and qa tools.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

&lt;p&gt;;&lt;/p&gt;

</description>
      <category>git</category>
      <category>gitsecret</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>Set up PHP QA tools and control them via make [Tutorial Part 5]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Fri, 01 Jul 2022 06:37:57 +0000</pubDate>
      <link>https://dev.to/pascallandau/set-up-php-qa-tools-and-control-them-via-make-tutorial-part-5-2lh9</link>
      <guid>https://dev.to/pascallandau/set-up-php-qa-tools-and-control-them-via-make-tutorial-part-5-2lh9</guid>
      <description>&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/php-qa-tools-make-docker/" rel="noopener noreferrer"&gt;Set up PHP QA tools and control them via make [Tutorial Part 5]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In the fifth part of this tutorial series on developing PHP on Docker we will &lt;strong&gt;setup some PHP code quality tools&lt;/strong&gt; and provide a convenient way to control them via GNU make.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/php-qa-tools-make-docker/run-qa-tools.gif" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphp-qa-tools-make-docker%2Frun-qa-tools.gif" alt="Run QA tools"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;br&gt;
FYI: Originally I wanted this tutorial to be a part of &lt;br&gt;
&lt;a href="https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/" rel="noopener noreferrer"&gt;Create a CI pipeline for dockerized PHP Apps&lt;/a&gt;&lt;br&gt;
because QA checks are imho vital part of a CI setup - but it kinda grew "too big" and took a way  too much space from, well, actually setting up the CI pipelines :)&lt;br&gt;
&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/ocM4ktjqwIg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.   You find the branch with the final result of this tutorial at &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-5-php-qa-tools-make-docker" rel="noopener noreferrer"&gt;part-5-php-qa-tools-make-docker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/" rel="noopener noreferrer"&gt;Run Laravel 9 on Docker in 2022&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/" rel="noopener noreferrer"&gt;Use git-secret to encrypt secrets in the repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
Introduction

&lt;ul&gt;
&lt;li&gt;
The QA tools

&lt;ul&gt;
&lt;li&gt;phpcs and phpcbf&lt;/li&gt;
&lt;li&gt;phpstan&lt;/li&gt;
&lt;li&gt;php-parallel-lint&lt;/li&gt;
&lt;li&gt;composer-require-checker&lt;/li&gt;
&lt;li&gt;Additional tools (out of scope)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

QA make targets

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;qa&lt;/code&gt; target&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;execute&lt;/code&gt; "function"&lt;/li&gt;
&lt;li&gt;Parallel execution and a helper target&lt;/li&gt;
&lt;li&gt;Sprinkle some color on top&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Further updates in the codebase&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Code quality tools ensure a &lt;strong&gt;baseline of code quality&lt;/strong&gt; by automatically checking certain rules, e.g. &lt;strong&gt;code style definitions&lt;/strong&gt;, proper &lt;strong&gt;usage of types&lt;/strong&gt;, proper &lt;strong&gt;declaration of dependencies&lt;/strong&gt;, etc. When run regularly they are a great way to enforce better code and are thus a&lt;br&gt;
&lt;strong&gt;perfect fit for a CI pipeline&lt;/strong&gt;. For this tutorial, I'm going to setup the following tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Style Checker: phpcs&lt;/li&gt;
&lt;li&gt;Static Analyzer: phpstan&lt;/li&gt;
&lt;li&gt;Code Linter: php-parallel-lint&lt;/li&gt;
&lt;li&gt;Dependency Checker: composer-require-checker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and provide convenient access through a &lt;code&gt;qa&lt;/code&gt; make target. The end result will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/php-qa-tools-make-docker/qa-tool-output.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphp-qa-tools-make-docker%2Fqa-tool-output.PNG" alt="QA tool output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FYI: When we started out with using code quality tools in general, we have used  &lt;a href="https://github.com/phpro/grumphp" rel="noopener noreferrer"&gt;GrumPHP&lt;/a&gt; - and I would still recommend it. We have only  transitioned away from it because &lt;code&gt;make&lt;/code&gt; gives us a little more flexibility and control.&lt;/p&gt;

&lt;p&gt;You can find the "final" makefile at &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part-5-php-qa-tools-make-docker/.make/01-02-application-qa.mk" rel="noopener noreferrer"&gt;&lt;code&gt;.make/01-02-application-qa.mk&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: The &lt;code&gt;Makefile&lt;/code&gt; is build on top of the setup that I introduced in &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;Docker from scratch for PHP 8.1 Applications in 2022&lt;/a&gt;,  so please refer to that tutorial if anything is not clear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;##@ [Application: QA]
&lt;/span&gt;
&lt;span class="c"&gt;# variables
&lt;/span&gt;&lt;span class="nv"&gt;CORES&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;  &lt;span class="o"&gt;||&lt;/span&gt; sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.ncpu&lt;span class="p"&gt;)&lt;/span&gt; 2&amp;gt; /dev/null&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# constants
&lt;/span&gt; &lt;span class="c"&gt;## files
&lt;/span&gt;&lt;span class="nv"&gt;ALL_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./
&lt;span class="nv"&gt;APP_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app/
&lt;span class="nv"&gt;TEST_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tests/

 &lt;span class="c"&gt;## bash colors
&lt;/span&gt;&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0&lt;span class="p"&gt;;&lt;/span&gt;31m
&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0&lt;span class="p"&gt;;&lt;/span&gt;32m
&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0&lt;span class="p"&gt;;&lt;/span&gt;33m
&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0m

&lt;span class="c"&gt;# Tool CLI config
&lt;/span&gt;&lt;span class="nv"&gt;PHPUNIT_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpunit
&lt;span class="nv"&gt;PHPUNIT_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; phpunit.xml
&lt;span class="nv"&gt;PHPUNIT_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PHPSTAN_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpstan analyse
&lt;span class="nv"&gt;PHPSTAN_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9
&lt;span class="nv"&gt;PHPSTAN_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_FILES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;TEST_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PHPCS_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpcs
&lt;span class="nv"&gt;PHPCS_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--parallel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;CORES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--standard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;psr12
&lt;span class="nv"&gt;PHPCS_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PHPCBF_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpcbf
&lt;span class="nv"&gt;PHPCBF_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_ARGS&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PHPCBF_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PARALLEL_LINT_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/parallel-lint
&lt;span class="nv"&gt;PARALLEL_LINT_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="nt"&gt;--exclude&lt;/span&gt; vendor/ &lt;span class="nt"&gt;--exclude&lt;/span&gt; .docker &lt;span class="nt"&gt;--exclude&lt;/span&gt; .git
&lt;span class="nv"&gt;PARALLEL_LINT_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;ALL_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;COMPOSER_REQUIRE_CHECKER_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/composer-require-checker
&lt;span class="nv"&gt;COMPOSER_REQUIRE_CHECKER_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--ignore-parse-errors&lt;/span&gt;

&lt;span class="c"&gt;# call with NO_PROGRESS=true to hide tool progress (makes sense when invoking multiple tools together)
&lt;/span&gt;&lt;span class="nv"&gt;NO_PROGRESS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(NO_PROGRESS),true)&lt;/span&gt;
    &lt;span class="nv"&gt;PHPSTAN_ARGS&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nt"&gt;--no-progress&lt;/span&gt;
    &lt;span class="nv"&gt;PARALLEL_LINT_ARGS&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nt"&gt;--no-progress&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="nv"&gt;PHPCS_ARGS&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;
    &lt;span class="nv"&gt;PHPCBF_ARGS&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;

&lt;span class="c"&gt;# Use NO_PROGRESS=false when running individual tools.
# On  NO_PROGRESS=true  the corresponding tool has no output on success
#                       apart from its runtime but it will still print 
#                       any errors that occured. 
&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt; &lt;span class="nv"&gt;execute&lt;/span&gt;
    &lt;span class="err"&gt;if&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$(NO_PROGRESS)"&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="err"&gt;];&lt;/span&gt; &lt;span class="err"&gt;then&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="err"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;else&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="nv"&gt;START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"%-35s"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nv"&gt;OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;%-6s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;END-START&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" took &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;YELLOW&lt;/span&gt;&lt;span class="p"&gt;)$${&lt;/span&gt;&lt;span class="s2"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;%-6s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;END-START&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" took &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;YELLOW&lt;/span&gt;&lt;span class="p"&gt;)$${&lt;/span&gt;&lt;span class="s2"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="s2"&gt;OUTPUT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;endef&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;
&lt;span class="nl"&gt;test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run all tests&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PHPUNIT_CMD&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PHPUNIT_ARGS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phplint&lt;/span&gt;
&lt;span class="nl"&gt;phplint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run phplint on all files&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;call execute,&lt;span class="p"&gt;$(&lt;/span&gt;PARALLEL_LINT_CMD&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PARALLEL_LINT_ARGS&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PARALLEL_LINT_FILES&lt;span class="p"&gt;)&lt;/span&gt;, &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpcs&lt;/span&gt;
&lt;span class="nl"&gt;phpcs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run style check on all application files&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;call execute,&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_CMD&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_ARGS&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_FILES&lt;span class="p"&gt;)&lt;/span&gt;, &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpcbf&lt;/span&gt;
&lt;span class="nl"&gt;phpcbf&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run style fixer on all application files&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;call execute,&lt;span class="p"&gt;$(&lt;/span&gt;PHPCBF_CMD&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPCBF_ARGS&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPCBF_FILES&lt;span class="p"&gt;)&lt;/span&gt;, &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan&lt;/span&gt;
&lt;span class="nl"&gt;phpstan&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run static analyzer on all application and test files &lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;call execute,&lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_CMD&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_ARGS&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_FILES&lt;span class="p"&gt;)&lt;/span&gt;, &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;composer-require-checker&lt;/span&gt;
&lt;span class="nl"&gt;composer-require-checker&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run dependency checker&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;call execute,&lt;span class="p"&gt;$(&lt;/span&gt;COMPOSER_REQUIRE_CHECKER_CMD&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;COMPOSER_REQUIRE_CHECKER_ARGS&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="s2"&gt;""&lt;/span&gt;, &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;qa&lt;/span&gt;
&lt;span class="nl"&gt;qa&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run code quality tools on all files&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;CORES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--no-print-directory&lt;/span&gt; &lt;span class="nt"&gt;--output-sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;target qa-exec &lt;span class="nv"&gt;NO_PROGRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;qa-exec&lt;/span&gt;
&lt;span class="nl"&gt;qa-exec&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan &lt;/span&gt;\
&lt;span class="nf"&gt;   phplint &lt;/span&gt;\
&lt;span class="nf"&gt;   composer-require-checker &lt;/span&gt;\
&lt;span class="nf"&gt;   phpcs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="the-qa-tools"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The QA tools
&lt;/h2&gt;

&lt;p&gt;&lt;a id="phpcs-and-phpcbf"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  phpcs and phpcbf
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;phpcs&lt;/code&gt; is the CLI tool of the style checker &lt;a href="https://github.com/squizlabs/PHP_CodeSniffer" rel="noopener noreferrer"&gt;squizlabs/PHP_CodeSniffer&lt;/a&gt;. It also comes with &lt;code&gt;phpcbf&lt;/code&gt; - a tool to automatically fix style errors.&lt;/p&gt;

&lt;p&gt;Installation via composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make composer &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"require --dev squizlabs/php_codesniffer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For now we will simply use the pre-configured ruleset for &lt;a href="https://www.php-fig.org/psr/psr-12/" rel="noopener noreferrer"&gt;PSR-12: Extended Coding Style&lt;/a&gt;. When run in the &lt;code&gt;application&lt;/code&gt; container for the first time on the &lt;code&gt;app&lt;/code&gt; directory via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vendor/bin/phpcs --standard=PSR12 --parallel=4 -p app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i.e.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--standard=PSR12 =&amp;gt; use the PSR12 ruleset
--parallel=4     =&amp;gt; run with 4 parallel processes
-p               =&amp;gt; show the progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we get the following result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/phpcs --standard=PSR12 --parallel=4 -p app

FILE: /var/www/app/app/Console/Kernel.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 1 LINE
----------------------------------------------------------------------
 28 | ERROR | [x] Expected at least 1 space before "."; 0 found
 28 | ERROR | [x] Expected at least 1 space after "."; 0 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------


FILE: /var/www/app/app/Http/Controllers/HomeController.php
----------------------------------------------------------------------
FOUND 4 ERRORS AFFECTING 2 LINES
----------------------------------------------------------------------
 37 | ERROR | [x] Expected at least 1 space before "."; 0 found
 37 | ERROR | [x] Expected at least 1 space after "."; 0 found
 45 | ERROR | [x] Expected at least 1 space before "."; 0 found
 45 | ERROR | [x] Expected at least 1 space after "."; 0 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------


FILE: /var/www/app/app/Jobs/InsertInDbJob.php
-------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
-------------------------------------------------------------------------------
 13 | ERROR | [x] Each imported trait must have its own "use" import statement
-------------------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
-------------------------------------------------------------------------------


FILE: /var/www/app/app/Models/User.php
-------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
-------------------------------------------------------------------------------
 13 | ERROR | [x] Each imported trait must have its own "use" import statement
-------------------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All errors can be fixed automatically with &lt;code&gt;phpcbf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/phpcbf --standard=PSR12 --parallel=4 -p app

PHPCBF RESULT SUMMARY
-------------------------------------------------------------------------
FILE                                                     FIXED  REMAINING
-------------------------------------------------------------------------
/var/www/app/app/Console/Kernel.php                      2      0
/var/www/app/app/Http/Controllers/HomeController.php     4      0
/var/www/app/app/Jobs/InsertInDbJob.php                  1      0
/var/www/app/app/Models/User.php                         1      0
-------------------------------------------------------------------------
A TOTAL OF 8 ERRORS WERE FIXED IN 4 FILES
-------------------------------------------------------------------------

Time: 411ms; Memory: 8MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a follow-up run of &lt;code&gt;phpcs&lt;/code&gt; doesn't show any more errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/phpcs --standard=PSR12 --parallel=4 -p app
.................... 20 / 20 (100%)


Time: 289ms; Memory: 8MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="phpstan"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  phpstan
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;phpstan&lt;/code&gt; is the CLI tool of the static code analyzer &lt;a href="https://github.com/phpstan/phpstan" rel="noopener noreferrer"&gt;phpstan/phpstan&lt;/a&gt; (see also the &lt;a href="https://phpstan.org/user-guide/getting-started" rel="noopener noreferrer"&gt;full PHPStan documentation&lt;/a&gt;). It provides some default "levels" of increasing strictness to report potential bugs based on the AST of the analyzed PHP code.&lt;/p&gt;

&lt;p&gt;Installation via composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make composer &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"require --dev phpstan/phpstan"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this is a "fresh" codebase with very little code let's go for the &lt;a href="https://phpstan.org/user-guide/rule-levels" rel="noopener noreferrer"&gt;highest level 9&lt;/a&gt; (as of 2022-04-24) and run it in the &lt;code&gt;application&lt;/code&gt; container on the &lt;code&gt;app&lt;/code&gt; and &lt;code&gt;tests&lt;/code&gt; directories via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vendor/bin/phpstan analyse app tests --level=9

--level=9        =&amp;gt; use level 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/phpstan analyse app tests --level=9
 25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ------------------------------------------------------------------------------------------------------------------
  Line   app/Commands/SetupDbCommand.php
 ------ ------------------------------------------------------------------------------------------------------------------
  22     Method App\Commands\SetupDbCommand::getOptions() return type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  34     Method App\Commands\SetupDbCommand::handle() has no return type specified.
 ------ ------------------------------------------------------------------------------------------------------------------

 ------ -------------------------------------------------------------------------------------------------------
  Line   app/Http/Controllers/HomeController.php
 ------ -------------------------------------------------------------------------------------------------------
  22     Parameter #1 $jobId of class App\Jobs\InsertInDbJob constructor expects string, mixed given.
  25     Part $jobId (mixed) of encapsed string cannot be cast to string.
  35     Call to an undefined method Illuminate\Redis\Connections\Connection::lRange().
  62     Call to an undefined method Illuminate\Contracts\View\Factory|Illuminate\Contracts\View\View::with().
 ------ -------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------
  Line   app/Http/Middleware/Authenticate.php
 ------ ------------------------------------------------------------------------------------------------------------------
  17     Method App\Http\Middleware\Authenticate::redirectTo() should return string|null but return statement is missing.
 ------ ------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   app/Http/Middleware/RedirectIfAuthenticated.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  26     Method App\Http\Middleware\RedirectIfAuthenticated::handle() should return Illuminate\Http\RedirectResponse|Illuminate\Http\Response but returns Illuminate\Http\RedirectResponse|Illuminate\Routing\Redirector.
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------
  Line   app/Jobs/InsertInDbJob.php
 ------ -----------------------------------------------------------------------
  22     Method App\Jobs\InsertInDbJob::handle() has no return type specified.
 ------ -----------------------------------------------------------------------

 ------ -------------------------------------------------
  Line   app/Providers/RouteServiceProvider.php
 ------ -------------------------------------------------
  36     PHPDoc tag @var above a method has no effect.
  36     PHPDoc tag @var does not specify variable name.
  60     Cannot access property $id on mixed.
 ------ -------------------------------------------------

 ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/Feature/App/Http/Controllers/HomeControllerTest.php
 ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------
  24     Method Tests\Feature\App\Http\Controllers\HomeControllerTest::test___invoke() has parameter $params with no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  38     Method Tests\Feature\App\Http\Controllers\HomeControllerTest::__invoke_dataProvider() return type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
 ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------------------------------------------------------
  Line   tests/TestCase.php
 ------ ---------------------------------------------------------------------------------------------------------------------
  68     Cannot access offset 'database' on mixed.
  71     Parameter #1 $config of method Illuminate\Database\Connectors\MySqlConnector::connect() expects array, mixed given.
 ------ ---------------------------------------------------------------------------------------------------------------------


 [ERROR] Found 16 errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After fixing (or ignoring :P) all errors, we now get&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/phpstan analyse app tests --level=9
25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

[OK] No errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="php-parallel-lint"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  php-parallel-lint
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;php-parallel-lint&lt;/code&gt; is the CLI tool of the PHP code linter &lt;a href="https://github.com/php-parallel-lint/PHP-Parallel-Lint" rel="noopener noreferrer"&gt;php-parallel-lint/PHP-Parallel-Lint&lt;/a&gt;. It ensures that all PHP files are syntactically correct.&lt;/p&gt;

&lt;p&gt;Installation via composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make composer &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"require --dev php-parallel-lint/php-parallel-lint"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Parallel" is already in the name, so we run it on the full codebase &lt;code&gt;./&lt;/code&gt; with 4 parallel processes  and exclude the &lt;code&gt;.git&lt;/code&gt; and &lt;code&gt;vendor&lt;/code&gt; directories to speed up the execution via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vendor/bin/parallel-lint -j 4 --exclude .git --exclude vendor ./
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i.e.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-j 4                              =&amp;gt; use 4 parallel processes
--exclude .git --exclude vendor   =&amp;gt; ignore the .git/ and vendor/ directories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we get&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/parallel-lint -j 4 --exclude .git --exclude vendor ./
PHP 8.1.1 | 4 parallel jobs
............................................................ 60/61 (98 %)
.                                                            61/61 (100 %)


Checked 61 files in 0.2 seconds
No syntax error found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No further TODOs here.&lt;/p&gt;

&lt;p&gt;&lt;a id="composer-require-checker"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  composer-require-checker
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;composer-require-checker&lt;/code&gt; is the CLI tool of the dependency checker &lt;a href="https://github.com/maglnet/ComposerRequireChecker" rel="noopener noreferrer"&gt;maglnet/ComposerRequireChecker&lt;/a&gt;. The tool ensures that the &lt;code&gt;composer.json&lt;/code&gt; file contains all dependencies that are used in the codebase.&lt;/p&gt;

&lt;p&gt;Installation via composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make composer &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"require --dev maglnet/composer-require-checker"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vendor/bin/composer-require-checker check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/composer-require-checker check
ComposerRequireChecker 4.0.0@baa11a4e9e5072117e3d180ef16c07036cafa4a2
The following 1 unknown symbols were found:
+---------------------------------------------+--------------------+
| Unknown Symbol                              | Guessed Dependency |
+---------------------------------------------+--------------------+
| Symfony\Component\Console\Input\InputOption |                    |
+---------------------------------------------+--------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's going on here?&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;Symfony\Component\Console\Input\InputOption&lt;/code&gt; in our &lt;code&gt;\App\Commands\SetupDbCommand&lt;/code&gt; and the code doesn't "fail" because &lt;code&gt;InputOption&lt;/code&gt; is defined in the&lt;code&gt;symfony/console&lt;/code&gt; package that is a&lt;br&gt;
&lt;strong&gt;transitive dependency&lt;/strong&gt; of &lt;code&gt;laravel/framework&lt;/code&gt;, see the &lt;a href="https://github.com/laravel/framework/blob/5b113dad7d2c88e15b65d987ca63f03b2be43e6a/composer.json#L34" rel="noopener noreferrer"&gt;&lt;code&gt;laravel/framework composer.json&lt;/code&gt;&lt;/a&gt; file. &lt;/p&gt;

&lt;p&gt;I.e. the &lt;code&gt;symfony/console&lt;/code&gt; package &lt;strong&gt;does actually exist&lt;/strong&gt; in our &lt;code&gt;vendor&lt;/code&gt; directory - but since we also use it &lt;em&gt;as a first-party-dependency directly in our code&lt;/em&gt;, we must declare the dependency explicitly. Otherwise, Laravel might at some point decide to drop &lt;code&gt;symfony/console&lt;/code&gt; and we would be left with broken code.&lt;/p&gt;

&lt;p&gt;To fix this, I run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make composer &lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"require symfony/console"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which will update the &lt;code&gt;composer.json&lt;/code&gt; file and add the dependency. Running &lt;code&gt;composer-require-checker&lt;/code&gt; again will now yield no further errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# vendor/bin/composer-require-checker check
ComposerRequireChecker 4.0.0@baa11a4e9e5072117e3d180ef16c07036cafa4a2
There were no unknown symbols found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="additional-tools-out-of-scope"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional tools (out of scope)
&lt;/h3&gt;

&lt;p&gt;In general, I'm a huge fan of code quality tools and we use them quite extensively. At some  point I'll probably dedicate a whole article to go over them in detail - but for now I'm just  gonna leave a list for inspiration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://packagist.org/packages/brianium/paratest" rel="noopener noreferrer"&gt;brianium/paratest&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Running PhpUnit tests in parallel&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/malukenho/mcbumpface" rel="noopener noreferrer"&gt;malukenho/mcbumpface&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Update the versions in the &lt;code&gt;composer.json&lt;/code&gt; file after an update&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/qossmic/deptrac-shim" rel="noopener noreferrer"&gt;qossmic/deptrac-shim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A shim for &lt;a href="https://packagist.org/packages/qossmic/deptrac" rel="noopener noreferrer"&gt;qossmic/deptrac&lt;/a&gt;: 
A tool to define dependency layers based on e.g. namespaces&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/icanhazstring/composer-unused" rel="noopener noreferrer"&gt;icanhazstring/composer-unused&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Show dependencies in the &lt;code&gt;composer.json&lt;/code&gt; that are not used in the codebase&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/roave/security-advisories" rel="noopener noreferrer"&gt;roave/security-advisories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gives a warning when packages with known vulnerabilities are used &lt;/li&gt;
&lt;li&gt;Alternative: &lt;a href="https://github.com/fabpot/local-php-security-checker/" rel="noopener noreferrer"&gt;local-php-security-checker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="qa-make-targets"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  QA make targets
&lt;/h2&gt;

&lt;p&gt;You might have noticed that &lt;strong&gt;all tools have their own configuration options&lt;/strong&gt;. Instead of remembering each of them, I'll define corresponding make targets in &lt;code&gt;.make/01-02-application-qa.mk&lt;/code&gt;. The easiest way to do so would be to "hard-code" the exact commands that I ran previously, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan&lt;/span&gt;
&lt;span class="nl"&gt;phpstan&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run static analyzer on all application and test files &lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; vendor/bin/phpstan analyse app tests &lt;span class="nt"&gt;--level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Please refer to the &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#run-commands-in-the-docker-containers" rel="noopener noreferrer"&gt;Run commands in the docker containers&lt;/a&gt; section in the previous tutorial for an explanation of the &lt;code&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/code&gt;  variable).&lt;/p&gt;

&lt;p&gt;However, this implementation is quite inflexible: What if we want to check a single file or try out other options? So let's create some variables instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;PHPSTAN_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpstan analyse
&lt;span class="nv"&gt;PHPSTAN_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9
&lt;span class="nv"&gt;PHPSTAN_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_FILES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;TEST_FILES&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan&lt;/span&gt;
&lt;span class="nl"&gt;phpstan&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run static analyzer on all application and test files &lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_CMD&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_ARGS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_FILES&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This target allows me to override the defaults and e.g. check only the file  &lt;code&gt;app/Commands/SetupDbCommand.php&lt;/code&gt; with &lt;code&gt;--level=1&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make phpstan &lt;span class="nv"&gt;PHPSTAN_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app/Commands/SetupDbCommand.php &lt;span class="nv"&gt;PHPSTAN_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--level=1"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make phpstan PHPSTAN_FILES=app/Commands/SetupDbCommand.php PHPSTAN_ARGS="--level=1" 
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 [OK] No errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remaining tool variables can be configured in the exact same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# constants
&lt;/span&gt; &lt;span class="c"&gt;## files
&lt;/span&gt;&lt;span class="nv"&gt;ALL_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./
&lt;span class="nv"&gt;APP_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app/
&lt;span class="nv"&gt;TEST_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tests/

&lt;span class="c"&gt;# Tool CLI config
&lt;/span&gt;&lt;span class="nv"&gt;PHPUNIT_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpunit
&lt;span class="nv"&gt;PHPUNIT_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; phpunit.xml
&lt;span class="nv"&gt;PHPUNIT_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;PHPSTAN_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpstan analyse
&lt;span class="nv"&gt;PHPSTAN_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9
&lt;span class="nv"&gt;PHPSTAN_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_FILES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;TEST_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PHPCS_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpcs
&lt;span class="nv"&gt;PHPCS_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--parallel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;CORES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--standard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;psr12
&lt;span class="nv"&gt;PHPCS_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PHPCBF_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/phpcbf
&lt;span class="nv"&gt;PHPCBF_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_ARGS&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PHPCBF_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;PHPCS_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;PARALLEL_LINT_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/parallel-lint
&lt;span class="nv"&gt;PARALLEL_LINT_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="nt"&gt;--exclude&lt;/span&gt; vendor/ &lt;span class="nt"&gt;--exclude&lt;/span&gt; .docker &lt;span class="nt"&gt;--exclude&lt;/span&gt; .git
&lt;span class="nv"&gt;PARALLEL_LINT_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;ALL_FILES&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;COMPOSER_REQUIRE_CHECKER_CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php vendor/bin/composer-require-checker
&lt;span class="nv"&gt;COMPOSER_REQUIRE_CHECKER_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--ignore-parse-errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="the-qa-target"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;qa&lt;/code&gt; target
&lt;/h3&gt;

&lt;p&gt;From a workflow perspective I usually want to &lt;strong&gt;run all configured qa tools&lt;/strong&gt; instead of each one individually (being able to run individually is still great if a tool fails, though).&lt;/p&gt;

&lt;p&gt;A trivial approach would be a new target that &lt;strong&gt;uses all individual tool targets as preconditions&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;qa&lt;/span&gt;
&lt;span class="nl"&gt;qa&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan &lt;/span&gt;\
&lt;span class="nf"&gt;   phplint &lt;/span&gt;\
&lt;span class="nf"&gt;   composer-require-checker &lt;/span&gt;\
&lt;span class="nf"&gt;   phpcs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But we can do better, because this target produces quite a noisy output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make qa
 25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 [OK] No errors

PHP 8.1.1 | 4 parallel jobs
............................................................ 60/61 (98 %)
.                                                            61/61 (100 %)


Checked 61 files in 0.3 seconds
No syntax error found
ComposerRequireChecker 4.0.0@baa11a4e9e5072117e3d180ef16c07036cafa4a2
There were no unknown symbols found.
.................... 20 / 20 (100%)


Time: 576ms; Memory: 8MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd rather have something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make qa
phplint                             done   took 1s
phpcs                               done   took 1s
phpstan                             done   took 3s
composer-require-checker            done   took 6s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="the-execute-function"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;execute&lt;/code&gt; "function"
&lt;/h3&gt;

&lt;p&gt;We'll make this work by &lt;strong&gt;suppressing the tool output&lt;/strong&gt; and &lt;strong&gt;using a user-defined &lt;code&gt;execute&lt;/code&gt; make function&lt;/strong&gt; to format all targets nicely. &lt;/p&gt;

&lt;p&gt;&lt;small&gt;Though "function" isn't quite correct here, because it's rather a &lt;a href="https://www.gnu.org/software/make/manual/html_node/Multi_002dLine.html#Multi_002dLine" rel="noopener noreferrer"&gt;multiline variable&lt;/a&gt; defined via &lt;code&gt;define ... endef&lt;/code&gt; that is then "invoked" via the &lt;a href="https://www.gnu.org/software/make/manual/html_node/Call-Function.html" rel="noopener noreferrer"&gt;call function&lt;/a&gt;.&lt;br&gt;
&lt;/small&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: 01-02-application-qa.mk
&lt;/span&gt;
&lt;span class="c"&gt;# call with NO_PROGRESS=true to hide tool progress (makes sense when invoking multiple tools together)
&lt;/span&gt;&lt;span class="nv"&gt;NO_PROGRESS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(NO_PROGRESS),true)&lt;/span&gt;
    &lt;span class="nv"&gt;PHPSTAN_ARGS&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nt"&gt;--no-progress&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;


&lt;span class="c"&gt;# Use NO_PROGRESS=false when running individual tools.
# On  NO_PROGRESS=true  the corresponding tool has no output on success
#                       apart from its runtime but it will still print 
#                       any errors that occured. 
&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt; &lt;span class="nv"&gt;execute&lt;/span&gt;
    &lt;span class="err"&gt;if&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$(NO_PROGRESS)"&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="err"&gt;];&lt;/span&gt; &lt;span class="err"&gt;then&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="err"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;else&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="nv"&gt;START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"%-35s"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nv"&gt;OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" %-6s"&lt;/span&gt; &lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;END-START&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" took &lt;/span&gt;&lt;span class="p"&gt;$${&lt;/span&gt;&lt;span class="s2"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" %-6s"&lt;/span&gt; &lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;END-START&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" took &lt;/span&gt;&lt;span class="p"&gt;$${&lt;/span&gt;&lt;span class="s2"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="s2"&gt;OUTPUT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;endef&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;NO_PROGRESS&lt;/code&gt; variable is set to &lt;code&gt;false&lt;/code&gt; by default and will cause a target to be invoked
as before, showing all its output immediately

&lt;ul&gt;
&lt;li&gt;if the variable is set to &lt;code&gt;true&lt;/code&gt;, the target is instead invoked via &lt;code&gt;eval&lt;/code&gt; and the output is
captured in the &lt;code&gt;OUTPUT&lt;/code&gt; bash variable that will only be printed if the result of the
invocation is faulty&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The tool targets are then adjusted to use the new function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan&lt;/span&gt;
&lt;span class="nl"&gt;phpstan&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run static analyzer on all application and test files&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;call execute,&lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_CMD&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_ARGS&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;PHPSTAN_FILES&lt;span class="p"&gt;)&lt;/span&gt;,&lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now call the &lt;code&gt;phpstan&lt;/code&gt; target with &lt;code&gt;NO_PROGRESS=true&lt;/code&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make phpstan NO_PROGRESS=true
phpstan                             done   took 4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An "error" would look likes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make phpstan NO_PROGRESS=true
phpstan                             fail   took 9s
 ------ ----------------------------------------
  Line   app/Providers/RouteServiceProvider.php
 ------ ----------------------------------------
  49     Cannot access property $id on mixed.
 ------ ----------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="parallel-execution-and-a-helper-target"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel execution and a helper target
&lt;/h3&gt;

&lt;p&gt;Technically, this also already works with the &lt;code&gt;qa&lt;/code&gt; target and we can even speed up the process by &lt;br&gt;
&lt;strong&gt;running the tools in parallel&lt;/strong&gt; with the &lt;a href="https://www.gnu.org/software/make/manual/html_node/Parallel.html" rel="noopener noreferrer"&gt;-j flag for "Parallel Execution"&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make -j 4 qa NO_PROGRESS=true
phpstan                            phplint                            composer-require-checker           phpcs                               done   took 5s
done   took 5s
done   took 7s
done   took 10s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well... not quite what we wanted. We also need to use  &lt;a href="https://www.gnu.org/software/make/manual/html_node/Parallel-Output.html" rel="noopener noreferrer"&gt;&lt;code&gt;--output-sync=target&lt;/code&gt; to controll the "Output During Parallel Execution"&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make -j 4 --output-sync=target qa NO_PROGRESS=true
phpstan                             done   took 3s
phplint                             done   took 1s
composer-require-checker            done   took 5s
phpcs                               done   took 1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this is quite a mouthful to type, we'll use a helper target &lt;code&gt;qa-exec&lt;/code&gt; for running the tools and put all the inconvenient-to-type options in the final &lt;code&gt;qa&lt;/code&gt; target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: 01-02-application-qa.mk
#...
&lt;/span&gt;
&lt;span class="c"&gt;# variables
&lt;/span&gt;&lt;span class="nv"&gt;CORES&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;  &lt;span class="o"&gt;||&lt;/span&gt; sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.ncpu&lt;span class="p"&gt;)&lt;/span&gt; 2&amp;gt; /dev/null&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;qa&lt;/span&gt;
&lt;span class="nl"&gt;qa&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run code quality tools on all files&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;CORES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--no-print-directory&lt;/span&gt; &lt;span class="nt"&gt;--output-sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;target qa-exec &lt;span class="nv"&gt;NO_PROGRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;qa-exec&lt;/span&gt;
&lt;span class="nl"&gt;qa-exec&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;phpstan &lt;/span&gt;\
&lt;span class="nf"&gt;   phplint &lt;/span&gt;\
&lt;span class="nf"&gt;   composer-require-checker &lt;/span&gt;\
&lt;span class="nf"&gt;   phpcs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the number of parallel processes I use &lt;code&gt;nproc&lt;/code&gt; (works on Linux and Windows) resp.  &lt;code&gt;sysctl -n hw.ncpu&lt;/code&gt; (works on Mac) to determine the number of available cores. If you dedicate  less resources to docker you might want to hard-code this setting to a lower value (e.g. by  adding a &lt;code&gt;CORES&lt;/code&gt; variable in  the &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env" rel="noopener noreferrer"&gt;&lt;code&gt;.make/.env&lt;/code&gt;&lt;/a&gt;  file).&lt;/p&gt;

&lt;p&gt;&lt;a id="sprinkle-some-color-on-top"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sprinkle some color on top
&lt;/h3&gt;

&lt;p&gt;The final piece for getting to the output mentioned in the Introduction is the  bash-coloring:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/php-qa-tools-make-docker/qa-tool-output.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphp-qa-tools-make-docker%2Fqa-tool-output.PNG" alt="QA tool output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To make this work, we need to understand first how colors work in bash:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This  [coloring] can be accomplished by adding a &lt;code&gt;\e&lt;/code&gt; [or &lt;code&gt;\033&lt;/code&gt;] at the beginning to form an &lt;br&gt;
escape sequence. The escape sequence for specifying color codes is &lt;code&gt;\e[COLORm&lt;/code&gt;&lt;br&gt;
(&lt;code&gt;COLOR&lt;/code&gt; represents our (numeric) color code in this case).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(via &lt;a href="https://dev.to/ifenna__/adding-colors-to-bash-scripts-48g4"&gt;Adding colors to Bash scripts&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;E.g. the following script will print a green text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0;32mThis text is green&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we define the required colors as variables and use them in the corresponding places in the &lt;code&gt;execute&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt; &lt;span class="c"&gt;## bash colors
&lt;/span&gt;&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0&lt;span class="p"&gt;;&lt;/span&gt;31m
&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0&lt;span class="p"&gt;;&lt;/span&gt;32m
&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0&lt;span class="p"&gt;;&lt;/span&gt;33m
&lt;span class="nv"&gt;NO_COLOR&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;33[0m

&lt;span class="c"&gt;# ...
&lt;/span&gt;
&lt;span class="c"&gt;# Use NO_PROGRESS=false when running individual tools.
# On  NO_PROGRESS=true  the corresponding tool has no output on success
#                       apart from its runtime but it will still print 
#                       any errors that occured. 
&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt; &lt;span class="nv"&gt;execute&lt;/span&gt;
    &lt;span class="err"&gt;if&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$(NO_PROGRESS)"&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="err"&gt;];&lt;/span&gt; &lt;span class="err"&gt;then&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="err"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;else&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="nv"&gt;START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"%-35s"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nv"&gt;OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;%-6s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;END-START&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" took &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;YELLOW&lt;/span&gt;&lt;span class="p"&gt;)$${&lt;/span&gt;&lt;span class="s2"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;%-6s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;END-START&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;" took &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;YELLOW&lt;/span&gt;&lt;span class="p"&gt;)$${&lt;/span&gt;&lt;span class="s2"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;NO_COLOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="s2"&gt;OUTPUT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;endef&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note, that i did &lt;strong&gt;not include the tests in the &lt;code&gt;qa&lt;/code&gt; target&lt;/strong&gt;. I like to run those  separately, because our tests usually take a lot longer to execute. So in my day-to-day work I  would run &lt;code&gt;make qa&lt;/code&gt; and &lt;code&gt;make test&lt;/code&gt; to ensure that code quality and tests are passing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make qa
phplint                             done   took 1s
phpcs                               done   took 1s
composer-require-checker            done   took 14s
phpstan                             done   took 16s

$ make test
PHPUnit 9.5.19 #StandWithUkraine

.......                                                             7 / 7 (100%)

Time: 00:03.772, Memory: 28.00 MB

OK (7 tests, 13 assertions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="further-updates-in-the-codebase"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further updates in the codebase
&lt;/h2&gt;

&lt;p&gt;I've also cleaned up the codebase a little in branch  &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-5-php-qa-tools-make-docker" rel="noopener noreferrer"&gt;part-5-php-qa-tools-make-docker&lt;/a&gt; and even though those changes have nothing to todo with "QA tools" I didn't want to leave them  unnoticed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;removing unnecessary files (&lt;code&gt;.styleci.yml&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;webpack.mix.js&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;removing unused values from the &lt;code&gt;.env.example&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;run a &lt;code&gt;composer update&lt;/code&gt; to get the latest Laravel version&lt;/li&gt;
&lt;li&gt;add a &lt;code&gt;show-help&lt;/code&gt; script to the &lt;code&gt;scripts&lt;/code&gt; section of the &lt;code&gt;composer.json&lt;/code&gt; file that references the 
&lt;code&gt;Makefile&lt;/code&gt; (see also &lt;a href="https://twitter.com/PascalLandau/status/1518227256648343552" rel="noopener noreferrer"&gt;this discussion on Twitter&lt;/a&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"show-help"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"make"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scripts-descriptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"show-help"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Display available 'make' commands (we use make instead of composer scripts)."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/php-qa-tools-make-docker/make-composer-scripts.gif" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphp-qa-tools-make-docker%2Fmake-composer-scripts.gif" alt="Run make via composer script"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replace &lt;code&gt;docker-compose&lt;/code&gt; with &lt;code&gt;docker compose&lt;/code&gt; to use &lt;a href="https://docs.docker.com/compose/cli-command/" rel="noopener noreferrer"&gt;compose v2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;For some reason, the last point caused some trouble because Linux and Docker Desktop for Windows  require a &lt;code&gt;-T&lt;/code&gt; flag for the &lt;code&gt;exec&lt;/code&gt; command to disable a TTY allocation in some cases. Whereas on  Docker Desktop for Mac  &lt;a href="https://github.com/moby/moby/issues/37366#issuecomment-527099456" rel="noopener noreferrer"&gt;the missing TTY lead to a cluttered output ("staircase effect")&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thus I modified the &lt;code&gt;Makefile&lt;/code&gt; to populate a &lt;code&gt;DOCKER_COMPOSE_EXEC_OPTIONS&lt;/code&gt; variable based on the OS&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add the -T options to "docker compose exec" to avoid the 
# "panic: the handle is invalid"
# error on Windows and Linux 
# @see https://stackoverflow.com/a/70856332/413531
&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_COMPOSE_EXEC_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-T&lt;/span&gt;

&lt;span class="c"&gt;# OS is defined for WIN systems, so "uname" will not be executed
&lt;/span&gt;&lt;span class="nv"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="nb"&gt;uname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(OS),Windows_NT)&lt;/span&gt;
    &lt;span class="c"&gt;# Windows requires the .exe extension, otherwise the entry is ignored
&lt;/span&gt; &lt;span class="c"&gt;# @see https://stackoverflow.com/a/60318554/413531
&lt;/span&gt; &lt;span class="nv"&gt;SHELL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; bash.exe
&lt;span class="err"&gt;else&lt;/span&gt; &lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(OS),Darwin)&lt;/span&gt;
    &lt;span class="c"&gt;# On Mac, the -T must be omitted to avoid cluttered output
&lt;/span&gt; &lt;span class="c"&gt;# @see https://github.com/moby/moby/issues/37366#issuecomment-401157643
&lt;/span&gt; &lt;span class="nv"&gt;DOCKER_COMPOSE_EXEC_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And use the variable when defining &lt;code&gt;EXECUTE_IN_*_CONTAINER&lt;/code&gt; in &lt;code&gt;.make/02-00-docker.mk&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(EXECUTE_IN_CONTAINER),true)&lt;/span&gt;
    &lt;span class="nv"&gt;EXECUTE_IN_ANY_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_EXEC_OPTIONS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_EXEC_OPTIONS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME_APPLICATION&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;EXECUTE_IN_WORKER_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_EXEC_OPTIONS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME_PHP_WORKER&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. You should now have a blueprint for adding code quality tools for your dockerized application and way to conveniently control them through a Makefile.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/" rel="noopener noreferrer"&gt;set up git secret to encrypt secret values and store them directly in the git repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>php</category>
      <category>docker</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Run Laravel 9 on Docker in 2022 [Tutorial Part 4]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Tue, 28 Jun 2022 07:35:36 +0000</pubDate>
      <link>https://dev.to/pascallandau/run-laravel-9-on-docker-in-2022-tutorial-part-4-1gmf</link>
      <guid>https://dev.to/pascallandau/run-laravel-9-on-docker-in-2022-tutorial-part-4-1gmf</guid>
      <description>&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/"&gt;Run Laravel 9 on Docker in 2022 [Tutorial Part 4]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In this third subpart of the fourth part of this tutorial series on developing PHP on Docker we will install &lt;strong&gt;Laravel and make sure our setup works for Artisan Commands, a Redis Queue and Controllers&lt;/strong&gt;&lt;br&gt;
for the front end requests.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/BpsBzpMD87c"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.   You find the branch for this tutorial at &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-4-3-run-laravel-9-docker-in-2022"&gt;part-4-3-run-laravel-9-docker-in-2022&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/"&gt;PhpStorm, Docker and Xdebug 3 on PHP 8.1 in 2022&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/php-qa-tools-make-docker/"&gt;Set up PHP QA tools and control them via make&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Install extensions&lt;/li&gt;
&lt;li&gt;Install Laravel&lt;/li&gt;
&lt;li&gt;
Update the PHP POC

&lt;ul&gt;
&lt;li&gt;
config

&lt;ul&gt;
&lt;li&gt;database connection&lt;/li&gt;
&lt;li&gt;queue connection&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Controllers&lt;/li&gt;
&lt;li&gt;Commands&lt;/li&gt;
&lt;li&gt;Jobs and workers&lt;/li&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Makefile updates

&lt;ul&gt;
&lt;li&gt;Clearing the queue&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Running the POC&lt;/li&gt;
&lt;li&gt;Wrapping up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The goal of this tutorial is to run the  &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#php-poc"&gt;PHP POC from part 4.1&lt;/a&gt; using Laravel as a framework instead of "plain PHP".  We'll use the newest version of Laravel (Laravel 9) that was &lt;a href="https://laravel-news.com/laravel-9-released"&gt;released at the beginning of February 2022&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="install-extensions"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install extensions
&lt;/h2&gt;

&lt;p&gt;Before Laravel can be installed, we need to add the necessary extensions of the framework (and all its dependencies) to the &lt;code&gt;php-base&lt;/code&gt; image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# File: .docker/images/php/base/Dockerfile

# ...

RUN apk add --update --no-cache  \
        php-curl~=${TARGET_PHP_VERSION} \

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="install-laravel"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Laravel
&lt;/h2&gt;

&lt;p&gt;We'll start by &lt;a href="https://laravel.com/docs/9.x/installation#installation-via-composer"&gt;creating a new Laravel project with composer&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer create-project --prefer-dist laravel/laravel /tmp/laravel "9.*" --no-install --no-scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The files are added to &lt;code&gt;/tmp/laravel&lt;/code&gt; because &lt;a href="https://github.com/composer/composer/issues/1135#issuecomment-10358244"&gt;composer projects cannot be created in non-empty folders&lt;/a&gt; , so we need to create the project in a temporary location first and move it afterwards.&lt;/p&gt;

&lt;p&gt;Since I don't have PHP 8 installed on my laptop, I'll execute the command in the &lt;code&gt;application&lt;/code&gt; docker container via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make execute-in-container DOCKER_SERVICE_NAME="application" COMMAND='composer create-project --prefer-dist laravel/laravel /tmp/laravel "9.*" --no-install --no-scripts'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then move the files into the application directory via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rm -rf public/ tests/ composer.* phpunit.xml
make execute-in-container DOCKER_SERVICE_NAME="application" COMMAND="bash -c 'mv -n /tmp/laravel/{.*,*} .' &amp;amp;&amp;amp; rm -f /tmp/laravel"
cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;composer install&lt;/code&gt; is skipped via &lt;code&gt;--no-install&lt;/code&gt; to avoid having to copy over the &lt;code&gt;vendor/&lt;/code&gt; folder
(which is super slow on Docker Desktop)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unix.stackexchange.com/a/127713"&gt;existing directories cannot be overwritten by &lt;code&gt;mv&lt;/code&gt;&lt;/a&gt; 
thus I remove &lt;code&gt;public/&lt;/code&gt; and &lt;code&gt;tests/&lt;/code&gt; upfront (as well as the &lt;code&gt;composer&lt;/code&gt; and &lt;code&gt;phpunit&lt;/code&gt; config 
files)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mv&lt;/code&gt; uses the &lt;code&gt;-n&lt;/code&gt; flag so that existing files like our &lt;code&gt;.editorconfig&lt;/code&gt; are not overwritten&lt;/li&gt;
&lt;li&gt;I need to use &lt;code&gt;bash -c&lt;/code&gt; to run the command in the container because otherwise  &lt;a href="https://github.com/moby/moby/issues/12558#issuecomment-94775867"&gt;the &lt;code&gt;*&lt;/code&gt; wildcard would have no effect in the container&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To finalize the installation I need to install the composer dependencies and execute the  &lt;code&gt;create-project&lt;/code&gt; scripts defined in  &lt;a href="https://github.com/laravel/laravel/blob/9.x/composer.json#L45"&gt;&lt;code&gt;composer.json&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make composer ARGS=install
make composer ARGS="run-script post-create-project-cmd"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since our nginx configuration was already pointing to the &lt;code&gt;public/&lt;/code&gt; directory, we can immediately  open &lt;a href="http://127.0.0.1"&gt;http://127.0.0.1&lt;/a&gt; in the browser and should see the frontpage of  a fresh Laravel installation.&lt;/p&gt;

&lt;p&gt;&lt;a id="update-the-php-poc"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Update the PHP POC
&lt;/h2&gt;

&lt;p&gt;&lt;a id="config"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  config
&lt;/h3&gt;

&lt;p&gt;We need to update the connection information for the database and the queue (previously  configured via &lt;code&gt;dependencies.php&lt;/code&gt;) in the &lt;code&gt;.env&lt;/code&gt; file&lt;/p&gt;

&lt;p&gt;&lt;a id="database-connection"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  database connection
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=application_db
DB_USERNAME=root
DB_PASSWORD=secret_mysql_root_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="queue-connection"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  queue connection
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUEUE_CONNECTION=redis

REDIS_HOST=redis
REDIS_PASSWORD=secret_redis_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="controllers"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Controllers
&lt;/h3&gt;

&lt;p&gt;The functionality of the previous &lt;code&gt;public/index.php&lt;/code&gt; file now lives in the &lt;code&gt;HomeController&lt;/code&gt; at  &lt;code&gt;app/Http/Controllers/HomeController.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;DispatchesJobs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;QueueManager&lt;/span&gt; &lt;span class="nv"&gt;$queueManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;DatabaseManager&lt;/span&gt; &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InsertInDbJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Adding item '&lt;/span&gt;&lt;span class="nv"&gt;$jobId&lt;/span&gt;&lt;span class="s2"&gt;' to queue"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"queue"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="cd"&gt;/**
             * @var RedisQueue $redisQueue
             */&lt;/span&gt;
            &lt;span class="nv"&gt;$redisQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$queueManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nv"&gt;$redisQueue&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRedis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$queueItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"queues:default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;99999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Items in queue&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;var_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$queueItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"db"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM jobs"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Items in db&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;var_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;HTML
            &amp;lt;ul&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a href="?dispatch=foo"&amp;gt;Dispatch job 'foo' to the queue.&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a href="?queue"&amp;gt;Show the queue.&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a href="?db"&amp;gt;Show the DB.&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
            HTML;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its content is displayed via the &lt;code&gt;home&lt;/code&gt; view located at &lt;code&gt;resources/views/home.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    {!! $content !!}
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller is added as a route in &lt;code&gt;routes/web.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;App\Http\Controllers\HomeController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="commands"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;

&lt;p&gt;We will replace the &lt;code&gt;setup.php&lt;/code&gt; script with a &lt;code&gt;SetupDbCommand&lt;/code&gt; that is located at  &lt;code&gt;app/Commands/SetupDbCommand.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SetupDbCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * @var string
     */&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:setup-db"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * @var string
     */&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Run the application database setup"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getOptions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;InputOption&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VALUE_NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"If given, the existing database tables are dropped and recreated."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$drop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Dropping all database tables..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WipeCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Done."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Running database migrations..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MigrateCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Done."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it the &lt;code&gt;AppServiceProvider&lt;/code&gt; in &lt;code&gt;app/Providers/AppServiceProvider.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;App\Commands\SetupDbCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and update the &lt;code&gt;setup-db&lt;/code&gt; target in &lt;code&gt;.make/01-00-application-setup.mk&lt;/code&gt; to run the &lt;code&gt;artisan&lt;/code&gt; Command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;setup-db&lt;/span&gt;
&lt;span class="nl"&gt;setup-db&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Setup the DB tables&lt;/span&gt;
    &lt;span class="nv"&gt;$(EXECUTE_IN_APPLICATION_CONTAINER)&lt;/span&gt; php artisan app:setup-db &lt;span class="nv"&gt;$(ARGS)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will also create a migration for the &lt;code&gt;jobs&lt;/code&gt; table in  &lt;code&gt;database/migrations/2022_02_10_000000_create_jobs_table.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Migration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jobs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="jobs-and-workers"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Jobs and workers
&lt;/h3&gt;

&lt;p&gt;We will replace the &lt;code&gt;worker.php&lt;/code&gt; script with &lt;code&gt;InsertInDbJob&lt;/code&gt; located at  &lt;code&gt;app/Jobs/InsertInDbJob.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InsertInDbJob&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$jobId&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;DatabaseManager&lt;/span&gt; &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO `jobs`(value) VALUES(?)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;though this will "only" handle the insertion part. For the worker itself we will use the native &lt;code&gt;\Illuminate\Queue\Console\WorkCommand&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php artisan queue:work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to adjust the &lt;code&gt;.docker/images/php/worker/Dockerfile&lt;/code&gt; and change&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG PHP_WORKER_COMMAND="php $APP_CODE_PATH/worker.php"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG PHP_WORKER_COMMAND="php $APP_CODE_PATH/artisan queue:work"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this change takes place directly in the Dockerfile, we must now rebuild the image&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make docker-build-image DOCKER_SERVICE_NAME=php-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and restart it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make docker-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="tests"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;I'd also like to take this opportunity to add a &lt;code&gt;Feature&lt;/code&gt; test for the &lt;code&gt;HomeController&lt;/code&gt; at &lt;code&gt;tests/Feature/App/Http/Controllers/HomeControllerTest.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeControllerTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setupDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setupQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * @dataProvider __invoke_dataProvider
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test___invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UrlGenerator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke_dataProvider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;"default"&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"params"&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
                &lt;span class="s2"&gt;"expected"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TEXT
                        &amp;lt;li&amp;gt;&amp;lt;a href="?dispatch=foo"&amp;gt;Dispatch job 'foo' to the queue.&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                        &amp;lt;li&amp;gt;&amp;lt;a href="?queue"&amp;gt;Show the queue.&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                        &amp;lt;li&amp;gt;&amp;lt;a href="?db"&amp;gt;Show the DB.&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                    TEXT&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s2"&gt;"database is empty"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"params"&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"db"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s2"&gt;"expected"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TEXT
                        Items in db
                    array (
                    )
                    TEXT&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s2"&gt;"queue is empty"&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"params"&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"queue"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s2"&gt;"expected"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TEXT
                        Items in queue
                    array (
                    )
                    TEXT&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_shows_existing_items_in_database&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$databaseManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DatabaseManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO `jobs` (id, value) VALUES(1, 'foo');"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UrlGenerator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"db"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$url&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TEXT
                Items in db
            array (
              0 =&amp;gt; 
              (object) array(
                 'id' =&amp;gt; 1,
                 'value' =&amp;gt; 'foo',
              ),
            )
            TEXT;&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_shows_existing_items_in_queue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$queueManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QueueManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InsertInDbJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"foo"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$queueManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UrlGenerator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"queue"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$url&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$expectedJobsCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TEXT
                Items in queue
            array (
              0 =&amp;gt; '{
            TEXT;&lt;/span&gt;

        &lt;span class="nv"&gt;$expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TEXT
            \\\\"jobId\\\\";s:3:\\\\"foo\\\\";
            TEXT;&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expectedJobsCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test checks the database as well as the queue and uses the helper methods  &lt;code&gt;$this-&amp;gt;setupDatabase()&lt;/code&gt; and &lt;code&gt;$this-&amp;gt;setupQueue()&lt;/code&gt; that I defined in the base test case at &lt;code&gt;tests/TestCase.php&lt;/code&gt; as follows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;   &lt;span class="cd"&gt;/**
     * @template T
     * @param class-string&amp;lt;T&amp;gt; $className
     * @return T
     */&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$className&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setupDatabase&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$databaseManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DatabaseManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$actualConnection&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDefaultConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$testingConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"testing"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$actualConnection&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$testingConnection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Database tests are only allowed to run on default connection '&lt;/span&gt;&lt;span class="nv"&gt;$testingConnection&lt;/span&gt;&lt;span class="s2"&gt;'. The current default connection is '&lt;/span&gt;&lt;span class="nv"&gt;$actualConnection&lt;/span&gt;&lt;span class="s2"&gt;'."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ensureDatabaseExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;artisan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SetupDbCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--drop"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setupQueue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$queueManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QueueManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$actualDriver&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$queueManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDefaultDriver&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$testingDriver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"testing"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$actualDriver&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$testingDriver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Queue tests are only allowed to run on default driver '&lt;/span&gt;&lt;span class="nv"&gt;$testingDriver&lt;/span&gt;&lt;span class="s2"&gt;'. The current default driver is '&lt;/span&gt;&lt;span class="nv"&gt;$actualDriver&lt;/span&gt;&lt;span class="s2"&gt;'."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;artisan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClearCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ensureDatabaseExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;DatabaseManager&lt;/span&gt; &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$databaseManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPdo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PDOException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// e.g. SQLSTATE[HY000] [1049] Unknown database 'testing'&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1049&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nv"&gt;$config&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nv"&gt;$connector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MySqlConnector&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$pdo&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connector&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$database&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDatabaseName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CREATE DATABASE IF NOT EXISTS `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$database&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The methods ensure that the tests are only executed if the proper database connection and queue  driver is configured. This is done through environment variables and I like using &lt;a href="https://laravel.com/docs/9.x/testing#the-env-testing-environment-file"&gt;a dedicated &lt;code&gt;.env&lt;/code&gt; file located at &lt;code&gt;.env.testing&lt;/code&gt;&lt;/a&gt;   for all testing &lt;code&gt;ENV&lt;/code&gt; values instead of defining them in the &lt;code&gt;phpunit.xml&lt;/code&gt; config file via &lt;code&gt;&amp;lt;env&amp;gt;&lt;/code&gt;  elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# File: .env.testing

DB_CONNECTION=testing
DB_DATABASE=testing
QUEUE_CONNECTION=testing
REDIS_DB=1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding connections have to be configured in the &lt;code&gt;config&lt;/code&gt; files&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: config/database.php&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="s1"&gt;'connections'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="s1"&gt;'testing'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mysql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DATABASE_URL'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_HOST'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_PORT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'3306'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_DATABASE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'testing'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'unix_socket'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_SOCKET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'charset'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'utf8mb4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'collation'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'utf8mb4_unicode_ci'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'prefix_indexes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'strict'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'engine'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;extension_loaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pdo_mysql'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MYSQL_ATTR_SSL_CA&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MYSQL_ATTR_SSL_CA'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="s1"&gt;'testing'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_URL'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_HOST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_PORT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_DB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1000'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File: config/queue.php&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="s1"&gt;'connections'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="s1"&gt;'testing'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'testing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// =&amp;gt; refers to the "database.redis.testing" config entry&lt;/span&gt;
            &lt;span class="s1"&gt;'queue'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_QUEUE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'retry_after'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'block_for'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'after_commit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests can be executed via &lt;code&gt;make test&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make test
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker vendor/bin/phpunit -c phpunit.xml
PHPUnit 9.5.13 by Sebastian Bergmann and contributors.

.......                                                             7 / 7 (100%)

Time: 00:02.709, Memory: 28.00 MB

OK (7 tests, 13 assertions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="makefile-updates"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Makefile updates
&lt;/h2&gt;

&lt;p&gt;&lt;a id="clearing-the-queue"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Clearing the queue
&lt;/h3&gt;

&lt;p&gt;For convenience while testing I added a make target to clear all items from the queue in  &lt;code&gt;.make/01-01-application-commands.mk&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clear-queue&lt;/span&gt;
&lt;span class="nl"&gt;clear-queue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Clear the job queue&lt;/span&gt;
    &lt;span class="nv"&gt;$(EXECUTE_IN_APPLICATION_CONTAINER)&lt;/span&gt; php artisan queue:clear &lt;span class="nv"&gt;$(ARGS)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="running-the-poc"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the POC
&lt;/h2&gt;

&lt;p&gt;Since the POC only uses &lt;code&gt;make&lt;/code&gt; targets and we basically just "refactored" them, there is no  modification necessary to make the existing &lt;code&gt;test.sh&lt;/code&gt; work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bash test.sh


  Building the docker setup


//...


  Starting the docker setup


//...


  Clearing DB


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application application php artisan app:setup-db --drop;
Dropping all database tables...
Dropped all tables successfully.
Done.
Running database migrations...
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (64.04ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (50.06ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (58.61ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (94.03ms)
Migrating: 2022_02_10_000000_create_jobs_table
Migrated:  2022_02_10_000000_create_jobs_table (31.85ms)
Done.


  Stopping workers


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker supervisorctl stop worker:*;
worker:worker_00: stopped
worker:worker_01: stopped
worker:worker_02: stopped
worker:worker_03: stopped


  Ensuring that queue and db are empty


&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    Items in queue
array (
)
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    Items in db
array (
)
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;


  Dispatching a job 'foo'


&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    Adding item 'foo' to queue
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;


  Asserting the job 'foo' is on the queue


&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    Items in queue
array (
  0 =&amp;gt; '{"uuid":"7ea63590-2a86-4739-abf8-8a059d41bd60","displayName":"App\\\\Jobs\\\\InsertInDbJob","job":"Illuminate\\\\Queue\\\\CallQueuedHandler@call","maxTries":null,"maxExceptions":null,"failOnTimeout":false,"backoff":null,"timeout":null,"retryUntil":null,"data":{"commandName":"App\\\\Jobs\\\\InsertInDbJob","command":"O:22:\\"App\\\\Jobs\\\\InsertInDbJob\\":11:{s:5:\\"jobId\\";s:3:\\"foo\\";s:3:\\"job\\";N;s:10:\\"connection\\";N;s:5:\\"queue\\";N;s:15:\\"chainConnection\\";N;s:10:\\"chainQueue\\";N;s:19:\\"chainCatchCallbacks\\";N;s:5:\\"delay\\";N;s:11:\\"afterCommit\\";N;s:10:\\"middleware\\";a:0:{}s:7:\\"chained\\";a:0:{}}"},"id":"I3k5PNyGZc6Z5XWCC4gt0qtSdqUZ84FU","attempts":0}',
)
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;


  Starting the workers


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker supervisorctl start worker:*;
worker:worker_00: started
worker:worker_01: started
worker:worker_02: started
worker:worker_03: started


  Asserting the queue is now empty


&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    Items in queue
array (
)
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;


  Asserting the db now contains the job 'foo'


&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
    Items in db
array (
  0 =&amp;gt;
  (object) array(
     'id' =&amp;gt; 1,
     'value' =&amp;gt; 'foo',
  ),
)
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. Laravel 9 should now be up and running on the previously set up docker infrastructure.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will &lt;a href="https://www.pascallandau.com/blog/php-qa-tools-make-docker/"&gt;Set up PHP QA tools and control them via make&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to build a Docker development setup for PHP Projects [Tutorial Part 1]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Mon, 27 Jun 2022 07:46:58 +0000</pubDate>
      <link>https://dev.to/pascallandau/how-to-build-a-docker-development-setup-for-php-projects-tutorial-part-1-10km</link>
      <guid>https://dev.to/pascallandau/how-to-build-a-docker-development-setup-for-php-projects-tutorial-part-1-10km</guid>
      <description>&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/" rel="noopener noreferrer"&gt;How to build a Docker development setup for PHP Projects [Tutorial Part 1]&lt;/a&gt;&lt;/p&gt;





  
    &lt;strong&gt;Caution&lt;/strong&gt;
  
  
    There is a &lt;strong&gt;follow-up version&lt;/strong&gt; of this article available that was published 
    in &lt;strong&gt;2022&lt;/strong&gt; - please make sure to read that as well:
    &lt;br&gt;
    &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;Docker from scratch for PHP 8.1 Applications in 2022&lt;/a&gt;
  


&lt;p&gt;In this part of my tutorial series on developing PHP on Docker we'll lay the fundamentals to build a complete development infrastructure and explain how to "structure" the Docker setup as part of a PHP project. Structure as in &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;folder structure ("what to put where")&lt;/li&gt;
&lt;li&gt;Dockerfile templates&lt;/li&gt;
&lt;li&gt;solving common problems (file permissions, runtime configuration, ...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will also create a minimal container setup consisting of php-fpm, nginx and a workspace container that we  refactor from the previous parts of this tutorial.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/YYI5mTjFDuA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my  &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.  The branch for this tutorial is  &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part_3_structuring-the-docker-setup-for-php-projects" rel="noopener noreferrer"&gt;part_3_structuring-the-docker-setup-for-php-projects&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/" rel="noopener noreferrer"&gt;Setting up PhpStorm with Xdebug for local development on Docker&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;Docker from scratch for PHP 8.1 Applications in 2022&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;
Structuring the repository

&lt;ul&gt;
&lt;li&gt;The .docker folder&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.shared&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-test.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The Makefile&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Defining services: php-fpm, nginx and workspace

&lt;ul&gt;
&lt;li&gt;
php-fpm

&lt;ul&gt;
&lt;li&gt;Modifying the pool configuration&lt;/li&gt;
&lt;li&gt;Custom ENTRYPOINT&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;nginx&lt;/li&gt;

&lt;li&gt;workspace (formerly php-cli)&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

Setting up docker-compose 

&lt;ul&gt;
&lt;li&gt;docker-compose.yml&lt;/li&gt;
&lt;li&gt;.env.example&lt;/li&gt;
&lt;li&gt;Building and running the containers&lt;/li&gt;
&lt;li&gt;Testing if everything works&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Makefile and &lt;code&gt;.bashrc&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;make&lt;/code&gt; as central entry point&lt;/li&gt;
&lt;li&gt;Install make on Windows (MinGW)&lt;/li&gt;
&lt;li&gt;Easy container access via &lt;code&gt;din&lt;/code&gt; .bashrc helper&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Fundamentals on building the containers

&lt;ul&gt;
&lt;li&gt;Understanding build context&lt;/li&gt;
&lt;li&gt;Dockerfile template&lt;/li&gt;
&lt;li&gt;Setting the timezone&lt;/li&gt;
&lt;li&gt;Synchronizing file and folder ownership on shared volumes&lt;/li&gt;
&lt;li&gt;
Modifying configuration files

&lt;ul&gt;
&lt;li&gt;Providing additional config files&lt;/li&gt;
&lt;li&gt;Changing non-static values&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Installing php extensions&lt;/li&gt;

&lt;li&gt;Installing common software&lt;/li&gt;

&lt;li&gt;Cleaning up&lt;/li&gt;

&lt;li&gt;

Using &lt;code&gt;ENTRYPOINT&lt;/code&gt; for pre-run configuration

&lt;ul&gt;
&lt;li&gt;Providing &lt;code&gt;host.docker.internal&lt;/code&gt; for linux host systems&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a id="introduction"&gt;&lt;/a&gt;Introduction
&lt;/h2&gt;

&lt;p&gt;When I started my current role as Head of Marketing Technology at ABOUT YOU back in 2016, we heavily relied on &lt;a href="https://www.pascallandau.com/blog/phpstorm-with-vagrant-using-laravel-homestead-on-windows-10/" rel="noopener noreferrer"&gt;Vagrant (namely: Homestead) as our development infrastructure&lt;/a&gt;.  Though that was much better than  working on our local machines, we've run into a couple of problems along the way (e.g. diverging software, bloated images, slow starting times, complicated readme for onboarding, upgrading php, ...).&lt;/p&gt;

&lt;p&gt;Today, everything that we need for the infrastructure is under source control and committed in the same repository that we use for our main application. In effect we get &lt;strong&gt;the same infrastructure for every developer&lt;/strong&gt; including automatic updates "for free". It is extremely easy to tinker around with updates / new tools due to the ephemeral nature of docker as tear down and rebuild only take one command and a couple of minutes.&lt;/p&gt;

&lt;p&gt;To get a feeling for how the process &lt;em&gt;feels&lt;/em&gt; like, simply execute the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/paslandau/docker-php-tutorial.git
cd docker-php-tutorial
git checkout part_3_structuring-the-docker-setup-for-php-projects
make docker-clean
make docker-init
make docker-build-from-scratch
make docker-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now have a running docker environment to develop PHP on docker (unless  something is blocking your port 80/443 or you don't have make installed&lt;br&gt;
;))&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;a id="structuring-the-repository"&gt;&lt;/a&gt;Structuring the repository
&lt;/h2&gt;

&lt;p&gt;While playing around with docker I've tried different ways to "structure" files and folders and ended up with the following concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;everything related to docker is &lt;strong&gt;placed in a &lt;code&gt;.docker&lt;/code&gt; directory on on the same level as the main application&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;in this directory

&lt;ul&gt;
&lt;li&gt;each service gets its own subdirectory for configuration&lt;/li&gt;
&lt;li&gt;is a &lt;strong&gt;&lt;code&gt;.shared&lt;/code&gt; folder containing scripts and configuration&lt;/strong&gt; required by multiple services&lt;/li&gt;
&lt;li&gt;is an &lt;strong&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/strong&gt; file containing variables for the &lt;strong&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;is a &lt;strong&gt;&lt;code&gt;docker-test.sh&lt;/code&gt;&lt;/strong&gt; file containing high level tests to validate the docker containers&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Makefile&lt;/strong&gt; with common instructions to control Docker is placed in the repository root&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;project&amp;gt;/
├── .docker/
|   ├── .shared/
|   |   ├── config/
|   |   └── scripts/
|   ├── php-fpm/
|   |   └── Dockerfile
|   ├── ... &amp;lt;additional services&amp;gt;/
|   ├── .env.example
|   ├── docker-compose.yml
|   └── docker-test.sh
├── Makefile
├── index.php
└──  ... &amp;lt;additional app files&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="the-docker-folder"&gt;&lt;/a&gt;The .docker folder
&lt;/h3&gt;

&lt;p&gt;As I mentioned, for me it makes a lot of sense to keep the &lt;strong&gt;infrastructure definition close to the codebase&lt;/strong&gt;, because it is immediately available to every developer. For bigger projects with multiple components there will be a code-infrastructure-coupling anyways  (e.g. in my experience it is usually not possible to simply switch MySQL for PostgreSQL without any other changes)  and for a library it is a very convenient (although opinionated) way to get started. &lt;/p&gt;

&lt;p&gt;I personally find it rather  frustrating when I want to contribute to an open source project but find myself spending a significant amount of time setting the environment up correctly instead of being able to just work on the code.&lt;/p&gt;

&lt;p&gt;Ymmv, though (e.g. because you don't want everybody with write access to your app repo also to be able to change your  infrastructure code). We actually went a different route previously and had a second repository ("-inf")  that would contain the contents of the &lt;code&gt;.docker&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;project-inf&amp;gt;/
├── .shared/
|   ├── config/
|   └── scripts/
├── php-fpm/
|   └── Dockerfile
├── ... &amp;lt;additional services&amp;gt;/
├── .env.example
└──  docker-compose.yml

&amp;lt;project&amp;gt;/
├── index.php
└──  ... &amp;lt;additional app files&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked as well, but we often ran into situations where  the contents of the repo would be stale for some devs, plus it was simply additional overhead with not other benefits  to us at that point. Maybe &lt;a href="https://medium.com/@porteneuve/mastering-git-submodules-34c65e940407" rel="noopener noreferrer"&gt;git submodules&lt;/a&gt; will enable us to get the best of both worlds - I'll blog about it once we try ;)&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="shared-folder"&gt;&lt;/a&gt;The &lt;code&gt;.shared&lt;/code&gt; folder
&lt;/h3&gt;

&lt;p&gt;When dealing with multiple services, chances are high that some of those services will be configured similarly, e.g. for&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;installing common software &lt;/li&gt;
&lt;li&gt;setting up unix users (with the same ids)&lt;/li&gt;
&lt;li&gt;configuration (think php-cli for workers and php-fpm for web requests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To avoid duplication, I place scripts (simple bash files) and config files in the &lt;code&gt;.shared&lt;/code&gt; folder and make it available in the build context for each service. I'll explain the process in more detail under  providing the correct build context.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id=""&gt;&lt;/a&gt;&lt;code&gt;docker-test.sh&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Is really just a simple bash script that includes some high level tests to make sure that the containers are built correctly. See section Testing if everything works.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="env-example-and-docker-compose-yml"&gt;&lt;/a&gt;&lt;code&gt;.env.example&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt; uses a &lt;a href="https://docs.docker.com/compose/environment-variables/#the-env-file" rel="noopener noreferrer"&gt;&lt;code&gt;.env&lt;/code&gt; file&lt;/a&gt; for a convenient way to define and  &lt;a href="https://docs.docker.com/compose/compose-file/#variable-substitution" rel="noopener noreferrer"&gt;&lt;code&gt;substitute environment variables&lt;/code&gt;&lt;/a&gt;.  Since this &lt;code&gt;.env&lt;/code&gt; file is environment specific, it is &lt;strong&gt;NOT&lt;/strong&gt;&lt;br&gt;
part of the repository (i.e. ignored via &lt;code&gt;.gitignore&lt;/code&gt;). Instead, we provide a &lt;code&gt;.env.example&lt;/code&gt; file that contains  the required environment variables including reasonable default values. A new dev would usually run  &lt;code&gt;cp .env.example .env&lt;/code&gt; after checking out the repository for the first time.  See section .env.example.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;a id="the-makefile"&gt;&lt;/a&gt;The Makefile &lt;code&gt;make&lt;/code&gt; and &lt;code&gt;Makefile&lt;/code&gt;s
&lt;/h3&gt;

&lt;p&gt;are among those things that I've heard about occasionally but never really cared to  understand (mostly because I associated them with C). Boy, did I miss out. I was comparing different strategies to provide code quality tooling (style checkers, static analyzers, tests, ...) and went from custom bash scripts over &lt;a href="https://getcomposer.org/doc/articles/scripts.md" rel="noopener noreferrer"&gt;composer scripts&lt;/a&gt; to finally end up at &lt;code&gt;Makefile&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Makefile&lt;/code&gt; serves as a central entry point and simplifies the management of the docker containers, e.g. for (re-)building, starting, stopping, logging in, etc. See section Makefile and .bashrc.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;a id="defining-services-php-fpm-nginx-and-workspace"&gt;&lt;/a&gt;Defining services: php-fpm, nginx and workspace
&lt;/h2&gt;

&lt;p&gt;Let's have a look at a real example and "refactor" the  php-cli, php-fpm and nginx containers from the &lt;a href="https://www.pascallandau.com/blog/php-php-fpm-and-nginx-on-docker-in-windows-10/" rel="noopener noreferrer"&gt;first part of this tutorial series&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This is the folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;project&amp;gt;/
├── .docker/
|   ├── .shared/
|   |   ├── config/
|   |   |   └── php/ 
|   |   |       └── conf.d/
|   |   |           └── zz-app.ini
|   |   └── scripts/
|   |       └── docker-entrypoint/
|   |           └── resolve-docker-host-ip.sh
|   ├── nginx/
|   |   ├── sites-available/
|   |   |   └── default.conf
|   |   ├── Dockerfile
|   |   └── nginx.conf
|   ├── php-fpm/
|   |   ├── php-fpm.d/
|   |   |   └── pool.conf
|   |   └── Dockerfile
|   ├── workspace/ (formerly php-cli)
|   |   ├── .ssh/
|   |   |   └── insecure_id_rsa
|   |   |   └── insecure_id_rsa.pub
|   |   └── Dockerfile
|   ├── .env.example
|   ├── docker-compose.yml
|   └── docker-test.sh
├── Makefile
└── index.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="php-fpm"&gt;&lt;/a&gt;php-fpm
&lt;/h3&gt;

&lt;p&gt;Click here to &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/.docker/php-fpm/Dockerfile" rel="noopener noreferrer"&gt;see the full php-fpm Dockerfile&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since we will be having two PHP containers, we need to place the common .ini settings in the &lt;code&gt;.shared&lt;/code&gt; directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|   ├── .shared/
|   |   ├── config/
|   |   |   └── php/ 
|   |   |       └── conf.d/
|   |   |           └── zz-app.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For now, &lt;code&gt;zz-app.ini&lt;/code&gt; will only contain our  &lt;a href="https://www.scalingphpbook.com/blog/2014/02/14/best-zend-opcache-settings.html" rel="noopener noreferrer"&gt;opcache setup&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; enable opcache
opcache.enable_cli = 1
opcache.enable = 1
opcache.fast_shutdown = 1
; revalidate everytime (effectively disabled for development)
opcache.validate_timestamps = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pool configuration is only relevant for php-fpm, so it goes in the directory of the service. Btw. I highly  recommend &lt;a href="https://serversforhackers.com/c/lemp-php-fpm-config" rel="noopener noreferrer"&gt;this video on PHP-FPM Configuration&lt;/a&gt; if your  php-fpm foo isn't already over 9000.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|   ├── php-fpm/
|   |   ├── php-fpm.d/
|   |   |   └── pool.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a id="modifying-the-pool-configuration"&gt;&lt;/a&gt;Modifying the pool configuration
&lt;/h4&gt;

&lt;p&gt;We're using the &lt;code&gt;modify_config.sh&lt;/code&gt; script to set the user and group that owns the php-fpm processes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# php-fpm pool config
COPY ${SERVICE_DIR}/php-fpm.d/* /usr/local/etc/php-fpm.d
RUN /tmp/scripts/modify_config.sh /usr/local/etc/php-fpm.d/zz-default.conf \
    "__APP_USER" \
    "${APP_USER}" \
 &amp;amp;&amp;amp; /tmp/scripts/modify_config.sh /usr/local/etc/php-fpm.d/zz-default.conf \
    "__APP_GROUP" \
    "${APP_GROUP}" \
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a id="custom-entrypoint"&gt;&lt;/a&gt;Custom ENTRYPOINT
&lt;/h4&gt;

&lt;p&gt;Since php-fpm needs to be debuggable, we need to ensure that the &lt;code&gt;host.docker.internal&lt;/code&gt; DNS entry exists, so we'll use the corresponding ENTRYPOINT to do that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# entrypoint
RUN mkdir -p /bin/docker-entrypoint/ \
 &amp;amp;&amp;amp; cp /tmp/scripts/docker-entrypoint/* /bin/docker-entrypoint/ \
 &amp;amp;&amp;amp; chmod +x -R /bin/docker-entrypoint/ \
;

ENTRYPOINT ["/bin/docker-entrypoint/resolve-docker-host-ip.sh","php-fpm"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="nginx"&gt;&lt;/a&gt;nginx
&lt;/h3&gt;

&lt;p&gt;Click here to &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/.docker/nginx/Dockerfile" rel="noopener noreferrer"&gt;see the full nginx Dockerfile&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The nginx setup is even simpler. There is no shared config, so that everything we need resides in&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|   ├── nginx/
|   |   ├── sites-available/
|   |   |   └── default.conf
|   |   ├── Dockerfile
|   |   └── nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note, that nginx only has the &lt;code&gt;nginx.conf&lt;/code&gt; file for configuration (i.e. there is no &lt;code&gt;conf.d&lt;/code&gt; directory or so), so we need to define the &lt;strong&gt;full&lt;/strong&gt; config in there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user __APP_USER __APP_GROUP;
worker_processes 4;
pid /run/nginx.pid;
daemon off;

http {
  # ...

  include /etc/nginx/sites-available/*.conf;

  # ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user and group are modified dynamically&lt;/li&gt;
&lt;li&gt;we specify &lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt; as the directory that holds the config files for the individual files via
&lt;code&gt;include /etc/nginx/sites-available/*.conf;&lt;/code&gt;
We need to keep the last point in mind, because we must use the same directory in the Dockerfile:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# nginx app config
COPY ${SERVICE_DIR}/sites-available/* /etc/nginx/sites-available/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The site's config file  &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/.docker/nginx/sites-available/default.conf" rel="noopener noreferrer"&gt;&lt;code&gt;default.conf&lt;/code&gt;&lt;/a&gt;  has a variable (&lt;code&gt;__NGINX_ROOT&lt;/code&gt;) for the &lt;code&gt;root&lt;/code&gt; directive and we "connect" it with the fpm-container via  &lt;code&gt;fastcgi_pass php-fpm:9000;&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    # ...
    root __NGINX_ROOT;
    # ...

    location ~ \.php$ {
        # ...
        fastcgi_pass php-fpm:9000;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;php-fpm&lt;/code&gt; will resolve to the &lt;code&gt;php-fpm&lt;/code&gt; container, because we use php-fpm as the service name in the docker-compose file, so it will be &lt;a href="https://docs.docker.com/compose/compose-file/#aliases" rel="noopener noreferrer"&gt;automatically used as the hostname&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Other containers on the same network can use either the service name or [an] alias to connect to one of the service’s containers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the Dockerfile, we use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG APP_CODE_PATH
RUN /tmp/scripts/modify_config.sh /etc/nginx/sites-available/default.conf \
    "__NGINX_ROOT" \
    "${APP_CODE_PATH}" \
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;APP_CODE_PATH&lt;/code&gt; will be passed via docker-compose when we build the container and mounted as a shared directory  from the host system. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="workspace-formerly-php-cli"&gt;&lt;/a&gt;workspace (formerly php-cli)
&lt;/h3&gt;

&lt;p&gt;Click here to &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/.docker/workspace/Dockerfile" rel="noopener noreferrer"&gt;see the full workspace Dockerfile&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We will use the former &lt;code&gt;php-cli&lt;/code&gt; container and make it our &lt;code&gt;workspace&lt;/code&gt; as introduced in part 2 of this tutorial under &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/#preparing-the-workspace-container" rel="noopener noreferrer"&gt;Preparing the "workspace" container&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This will be the container we use to point our IDE to, e.g. to execute tests. Its Dockerfile looks almost identical to the one of the &lt;code&gt;php-fpm&lt;/code&gt; service, apart from the SSH setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# set up ssh
RUN apt-get update -yqq &amp;amp;&amp;amp; apt-get install -yqq openssh-server \
 &amp;amp;&amp;amp; mkdir /var/run/sshd \
;

# add default public key to authorized_keys
USER ${APP_USER}
COPY ${SERVICE_DIR}/.ssh/insecure_id_rsa.pub /tmp/insecure_id_rsa.pub
RUN mkdir -p ~/.ssh \
 &amp;amp;&amp;amp; cat /tmp/insecure_id_rsa.pub &amp;gt;&amp;gt; ~/.ssh/authorized_keys \
 &amp;amp;&amp;amp; chown -R ${APP_USER}: ~/.ssh \
 &amp;amp;&amp;amp; chmod 700 ~/.ssh \
 &amp;amp;&amp;amp; chmod 600 ~/.ssh/authorized_keys \
;
USER root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;a id="setting-up-docker-compose"&gt;&lt;/a&gt;Setting up docker-compose
&lt;/h2&gt;

&lt;p&gt;In order to orchestrate the build process, we'll use docker-compose.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="docker-compose-yml"&gt;&lt;/a&gt;docker-compose.yml
&lt;/h3&gt;

&lt;p&gt;See the full  &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/.docker/docker-compose.yml" rel="noopener noreferrer"&gt;docker-compose.yml file in the repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each service uses &lt;code&gt;context: .&lt;/code&gt; so it has access to the &lt;code&gt;.shared&lt;/code&gt; folder.
The context is always &lt;a href="https://docs.docker.com/compose/extends/#understanding-multiple-compose-files" rel="noopener noreferrer"&gt;relative to the location of the first docker-compose.yml file&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;all arguments that we used in the Dockerfiles are defined in the &lt;code&gt;args:&lt;/code&gt;
section via
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  args:
    - APP_CODE_PATH=${APP_CODE_PATH_CONTAINER}
    - APP_GROUP=${APP_GROUP}
    - APP_GROUP_ID=${APP_GROUP_ID}
    - APP_USER=${APP_USER}
    - APP_USER_ID=${APP_USER_ID}
    - TZ=${TIMEZONE}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the codebase is synced from the host in all containers via
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  volumes:
    - ${APP_CODE_PATH_HOST}:${APP_CODE_PATH_CONTAINER}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;nginx&lt;/code&gt; service exposes ports on the host machine so that we can 
access the containers from "outside" via
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ports:
    - "${NGINX_HOST_HTTP_PORT}:80"
    - "${NGINX_HOST_HTTPS_PORT}:443"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;all services are part of the &lt;code&gt;backend&lt;/code&gt; network so they can talk to each
other. The &lt;code&gt;nginx&lt;/code&gt; service has an additional alias that allows us to
define an arbitrary host name via
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  networks:
    backend:
      aliases:
        - ${APP_HOST}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer to have a dedicated hostname per project (e.g. &lt;code&gt;docker-php-tutorial.local&lt;/code&gt;)&lt;br&gt;
  instead of using &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;localhost&lt;/code&gt; directly&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;a id="env-example"&gt;&lt;/a&gt;.env.example
&lt;/h3&gt;

&lt;p&gt;To fill in all the required variables / arguments, we're using a &lt;code&gt;.env.example&lt;/code&gt; file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Default settings for docker-compose
COMPOSE_PROJECT_NAME=docker-php-tutorial
COMPOSE_FILE=docker-compose.yml
COMPOSE_CONVERT_WINDOWS_PATHS=1

# build
PHP_VERSION=7.3
TIMEZONE=UTC
NETWORKS_DRIVER=bridge

# application
APP_USER=www-data
APP_GROUP=www-data
APP_USER_ID=1000
APP_GROUP_ID=1000
APP_CODE_PATH_HOST=../
APP_CODE_PATH_CONTAINER=/var/www/current

# required so we can reach the nginx server from other containers via that hostname
APP_HOST=docker-php-tutorial.local

# nginx
NGINX_HOST_HTTP_PORT=80
NGINX_HOST_HTTPS_PORT=443

# workspace
WORKSPACE_HOST_SSH_PORT=2222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;COMPOSE_&lt;/code&gt; variables in the beginning set some reasonable  &lt;a href="https://docs.docker.com/compose/reference/envvars/#compose_file" rel="noopener noreferrer"&gt;defaults for docker-compose&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="building-and-running-the-containers"&gt;&lt;/a&gt;Building and running the containers By now, we should have everything we need set up to get our dockerized PHP development up and running. If you haven't done it already, now would be a great time to clone  &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;the repository&lt;/a&gt; and checkout the  &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects" rel="noopener noreferrer"&gt;&lt;code&gt;part_3_structuring-the-docker-setup-for-php-projects&lt;/code&gt; branch&lt;/a&gt;:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/paslandau/docker-php-tutorial.git
cd docker-php-tutorial
git checkout part_3_structuring-the-docker-setup-for-php-projects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now copy the &lt;code&gt;.env.exmaple&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;. All the default values  should work out of the box - unless you already have something running on port &lt;code&gt;80&lt;/code&gt; or &lt;code&gt;443&lt;/code&gt;. In that case you have to change &lt;code&gt;NGINX_HOST_HTTP_PORT / NGINX_HOST_HTTP_PORT&lt;/code&gt; to a free port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can examine the "final" docker-compose.yml &lt;strong&gt;after&lt;/strong&gt; the variable substitution via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose -f .docker/docker-compose.yml --project-directory .docker config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;networks:
  backend:
    driver: bridge
services:
  nginx:
    build:
      args:
        APP_CODE_PATH: /var/www/current
        APP_GROUP: www-data
        APP_GROUP_ID: '1000'
        APP_USER: www-data
        APP_USER_ID: '1000'
        TZ: UTC
      context: D:\codebase\docker-php-tutorial\.docker
      dockerfile: ./nginx/Dockerfile
    image: php-docker-tutorial/nginx
    networks:
      backend:
        aliases:
        - docker-php-tutorial.local
    ports:
    - published: 80
      target: 80
    - published: 443
      target: 443
    volumes:
    - /d/codebase/docker-php-tutorial:/var/www/current:rw
  php-fpm:
// ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Note, that this command is run from &lt;code&gt;./docker-php-tutorial&lt;/code&gt;. If we would run this from &lt;code&gt;./docker-php-tutorial/.docker&lt;/code&gt;, we could simply use &lt;code&gt;docker-compose config&lt;/code&gt; - but since we'll define that in a Makefile later anyway, the additional "verbosity" won't matter ;)&lt;/p&gt;

&lt;p&gt;This command is also a great way to check the various paths that are resolved to their absolute form, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context: D:\codebase\docker-php-tutorial\.docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volumes:
- /d/codebase/docker-php-tutorial:/var/www/current:rw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual build is triggered via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose -f .docker/docker-compose.yml --project-directory .docker build --parallel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Since we have more than one container, it makes sense to build with  &lt;a href="https://docs.docker.com/compose/reference/build/" rel="noopener noreferrer"&gt;&lt;code&gt;--parallel&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To start the containers, we use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose -f .docker/docker-compose.yml --project-directory .docker up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 and should see&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker-compose -f .docker/docker-compose.yml --project-directory .docker up -d
Starting docker-php-tutorial_nginx_1     ... done
Starting docker-php-tutorial_workspace_1 ... done
Starting docker-php-tutorial_php-fpm_1   ... done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="testing-if-everything-works"&gt;&lt;/a&gt;Testing
&lt;/h3&gt;

&lt;p&gt;if everything works After rewriting our own docker setup a couple of times, I've come to appreciate a structured way to test if  "everything" works. Everything as in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are all containers running?&lt;/li&gt;
&lt;li&gt;does "host.docker.internal" exist?&lt;/li&gt;
&lt;li&gt;do we see the correct output when sending a request to nginx/php-fpm?&lt;/li&gt;
&lt;li&gt;are all required php extensions installed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This might seem superfluous (after all, we just defined excatly that in the Dockerfiles), but there will come a time when you (or someone else) need to make changes (new PHP version, new extensions, etc.) and having something that runs automatically and informs you about obvious flaws is a real time saver. &lt;/p&gt;

&lt;p&gt;You can see  &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/.docker/docker-test.sh" rel="noopener noreferrer"&gt;the full test file in the repository&lt;/a&gt;.  Since my bash isn't the best, I try to keep it as simple as possible. The tests can be run via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sh .docker/docker-test.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and should yield something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Testing service 'workspace'
=======
Checking if 'workspace' has a running container
OK
Testing PHP version '7.3' on 'workspace' for 'php' and expect to see 'PHP 7.3'
OK
Testing PHP module 'xdebug' on 'workspace' for 'php'
OK
Testing PHP module 'Zend OPcache' on 'workspace' for 'php'
OK
Checking 'host.docker.internal' on 'workspace'
OK

Testing service 'php-fpm'
=======
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;a id="makefile-and-bashrc"&gt;&lt;/a&gt;Makefile and &lt;code&gt;.bashrc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In the previous sections I have introduced a couple of commands, e.g. for building and running containers. And to be honest, I find it kinda challenging to keep them in mind without having to look up the exact options and arguments. I would usually create a helper function or an alias in my local &lt;code&gt;.bashrc&lt;/code&gt; file in a situation like that - but that wouldn't be available to other members of the team then and it would be very specific to this one project.  Instead we'll provide a &lt;code&gt;Makefile&lt;/code&gt; as a central reference point.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="using-make-as-central-entry-point"&gt;&lt;/a&gt;Using &lt;code&gt;make&lt;/code&gt; as central entry point
&lt;/h3&gt;

&lt;p&gt;Please refer &lt;a href="https://github.com/paslandau/docker-php-tutorial/blob/part_3_structuring-the-docker-setup-for-php-projects/Makefile" rel="noopener noreferrer"&gt;to the repository for the full &lt;code&gt;Makefile&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Going into the details of &lt;code&gt;make&lt;/code&gt; is a little out of scope for this article,  so I kindly refer to some articles that helped me get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://localheinz.com/blog/2018/01/24/makefile-for-lazy-developers/" rel="noopener noreferrer"&gt;Makefile for lazy developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.theodo.fr/2018/05/why-you-need-a-makefile-on-your-project/" rel="noopener noreferrer"&gt;Why you Need a Makefile on your Project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are written with a PHP context in mind.  Tip: If you are using PhpStorm, give the &lt;a href="https://plugins.jetbrains.com/plugin/9333-makefile-support" rel="noopener noreferrer"&gt;Makefile support plugin&lt;/a&gt; a try. And don't forget the number one rule:  &lt;a href="https://stackoverflow.com/q/14109724" rel="noopener noreferrer"&gt;A &lt;code&gt;Makefile&lt;/code&gt; requires tabs&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Note: If you are using Windows, &lt;code&gt;make&lt;/code&gt; is probably not available. See  Install make on Windows (MinGW) for instructions to set it up.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Makefile&lt;/code&gt; ist located in the root of the application. Since we use a &lt;code&gt;help&lt;/code&gt; target that makes the  &lt;a href="https://suva.sh/posts/well-documented-makefiles/" rel="noopener noreferrer"&gt;&lt;code&gt;Makefile&lt;/code&gt; self-documenting&lt;/a&gt;, we can simply run &lt;code&gt;make&lt;/code&gt; to see all the available commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make

Usage:
  make &amp;lt;target&amp;gt;

[Docker] Build / Infrastructure
  docker-clean                 Remove the .env file for docker
  docker-init                  Make sure the .env file exists for docker
  docker-build-from-scratch    Build all docker images from scratch, without cache etc. Build a specific image by providing the service name via: make docker-build CONTAINER=&amp;lt;service&amp;gt;
  docker-build                 Build all docker images. Build a specific image by providing the service name via: make docker-build CONTAINER=&amp;lt;service&amp;gt;
  docker-up                    Start all docker containers. To only start one container, use CONTAINER=&amp;lt;service&amp;gt;
  docker-down                  Stop all docker containers. To only stop one container, use CONTAINER=&amp;lt;service&amp;gt;
  docker-test                  Run the infrastructure tests for the docker setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a new developer, your "onboarding" to get a running infrastructure should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make docker-clean
make docker-init
make docker-build-from-scratch
make docker-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="install-make-on-windows-mingw"&gt;&lt;/a&gt;Install make on Windows (MinGW)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;make&lt;/code&gt; doesn't exist on Windows and is also not part of the standard installation of MinGW  (click here &lt;a href="https://www.pascallandau.com/blog/phpstorm-with-vagrant-using-laravel-homestead-on-windows-10/#git-and-git-bash" rel="noopener noreferrer"&gt;to learn how to setup MinGW&lt;/a&gt;) Setting is up is straight forward but as with "everything UI" it's easier if you can  actually "see what I'm doing" - so here's a video:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/taCJhnBXG_w"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The steps are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up &lt;code&gt;mingw-get&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Instructions: &lt;a href="https://web.archive.org/web/20200226035236/http://www.mingw.org/wiki/getting_started#toc5" rel="noopener noreferrer"&gt;https://web.archive.org/web/20200226035236/http://www.mingw.org/wiki/getting_started#toc5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Download: &lt;a href="https://sourceforge.net/projects/mingw/files/Installer/mingw-get-setup.exe/download" rel="noopener noreferrer"&gt;https://sourceforge.net/projects/mingw/files/Installer/mingw-get-setup.exe/download&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install and &lt;a href="https://www.pascallandau.com/blog/php7-with-xdebug-2-4-for-phpstorm-on-windows-10/#the-path-variable" rel="noopener noreferrer"&gt;add the &lt;code&gt;bin/&lt;/code&gt; directory to &lt;code&gt;PATH&lt;/code&gt; (shortcut &lt;code&gt;systempropertiesadvanced&lt;/code&gt;)&lt;/a&gt;.
Notes: 

&lt;ul&gt;
&lt;li&gt;Do not use an installation path that contains spaces!&lt;/li&gt;
&lt;li&gt;The installation path can be different from your MinGW location&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Install &lt;code&gt;mingw32-make&lt;/code&gt; via
&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  mingw-get install mingw32-make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create the file &lt;code&gt;bin/make&lt;/code&gt; with the content&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mingw32-make.exe $*
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Note: Sometimes Windows won't recognize non-.exe files - so instead of &lt;code&gt;bin/make&lt;/code&gt; you might need to name the file&lt;br&gt;
&lt;code&gt;bin/make.exe&lt;/code&gt; (with the same content)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open a new shell and type &lt;code&gt;make&lt;/code&gt;. The output should look something like this
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ make
  mingw32-make: *** Keine Targets angegeben und keine ¦make¦-Steuerdatei gefunden.  Schluss.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="easy-container-access-via-din-bashrc-helper"&gt;&lt;/a&gt;Easy container access via &lt;code&gt;din&lt;/code&gt; .bashrc helper
&lt;/h3&gt;

&lt;p&gt;I've got one last goodie for working with Docker that I use all the time: &lt;/p&gt;

&lt;p&gt;Logging into a running container via &lt;a href="https://docs.docker.com/engine/reference/commandline/exec/#run-docker-exec-on-a-running-container" rel="noopener noreferrer"&gt;&lt;code&gt;docker exec&lt;/code&gt;&lt;/a&gt; and the &lt;code&gt;din / dshell&lt;/code&gt; helper.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/structuring-the-docker-setup-for-php-projects/easy-docker-login-din.gif.png" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fstructuring-the-docker-setup-for-php-projects%2Feasy-docker-login-din.gif" alt="Log into any running docker container via din helper"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To make this work, put the following code in your &lt;code&gt;.bashrc&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function din() {
  filter=$1

  user=""
  if [[ -n "$2" ]];
  then
    user="--user $2"
  fi

  shell="bash"
  if [[ -n "$3" ]];
  then
    shell=$3
  fi

  prefix=""
  if [[ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]]; then
    prefix="winpty"
  fi
  ${prefix} docker exec -it ${user} $(docker ps --filter name=${filter} -q | head -1) ${shell}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$(docker ps --filter name=${filter} -q | head -1)&lt;/code&gt; part will find partial matches on running containers  for the first argument and pass the result to the &lt;code&gt;docker exec&lt;/code&gt; command. In effect, we can log into any container  by only providing a minimal matching string on the container name. E.g. to log in the &lt;code&gt;workspace&lt;/code&gt; container I can now simply type &lt;code&gt;din works&lt;/code&gt; from &lt;em&gt;anywhere&lt;/em&gt; on my system.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a id="fundamentals-on-building-the-containers"&gt;&lt;/a&gt;Fundamentals on building the containers
&lt;/h2&gt;

&lt;p&gt;Since we have now "seen" the end result, let's take a closer look behind the scenes.  I assume that you are already somewhat familiar with &lt;code&gt;Dockerfile&lt;/code&gt;s and have used &lt;code&gt;docker-compose&lt;/code&gt; to orchestrate multiple  services (if not, check out  &lt;a href="https://dev.toblog/php-php-fpm-and-nginx-on-docker-in-windows-10/#dockerfile"&gt;Persisting image changes with a Dockerfile&lt;/a&gt; and &lt;a href="https://dev.toblog/php-php-fpm-and-nginx-on-docker-in-windows-10/#docker-compose"&gt;Putting it all together: Meet docker-compose&lt;/a&gt;). But there are some points I would like to cover in a little more detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="understanding-build-context"&gt;&lt;/a&gt;Understanding build context
&lt;/h3&gt;

&lt;p&gt;There are two essential parts when building a container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Dockerfile&lt;/li&gt;
&lt;li&gt;the build context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read about the official description in the &lt;a href="http://docs.docker.com/engine/reference/builder/#usage" rel="noopener noreferrer"&gt;Dockerfile reference&lt;/a&gt;. You'll usually see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 which assumes that you use the current directory as build context and that there is a Dockerfile in the same directory.&lt;/p&gt;

&lt;p&gt;But you can also start the build via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build .docker -f .docker/nginx/Dockerfile
                 |      |
                 |      └── use the Dockerfile at ".docker/nginx/Dockerfile"
                 |
                 └── use the .docker subdirectory as build context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For me, the gist is this: The build context defines the files and folders (recursively) on your machine that are send  from the &lt;a href="https://docs.docker.com/engine/reference/commandline/cli/" rel="noopener noreferrer"&gt;Docker CLI&lt;/a&gt; to  the &lt;a href="https://docs.docker.com/engine/reference/commandline/dockerd/" rel="noopener noreferrer"&gt;Docker Daemon&lt;/a&gt; that executes the build process of a container so that you can reference those files in the Dockerfile (e.g. via &lt;code&gt;COPY&lt;/code&gt;). Take the following structure for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;project&amp;gt;/
├── .docker/
    ├── .shared/
    |   └── scripts/
    |       └── ...
    └── nginx/
        ├── nginx.conf
        └── Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assume, that the current working directory is &lt;code&gt;&amp;lt;project&amp;gt;/&lt;/code&gt;. If we started a build via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build .docker/nginx -f .docker/nginx/Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 the context would &lt;strong&gt;not&lt;/strong&gt; include the &lt;code&gt;.shared&lt;/code&gt; folder so we wouldn't be able to &lt;code&gt;COPY&lt;/code&gt; the &lt;code&gt;scripts/&lt;/code&gt; subfolder.  If we ran&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build .docker -f .docker/nginx/Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 however, that would make the &lt;code&gt;.shared&lt;/code&gt; folder available. In the Dockerfile itself, I need to know what the build context is, because I need to adjust the paths accordingly. Concrete example for the folder structure above and build  triggered via &lt;code&gt;docker build .docker -f .docker/nginx/Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM:nginx

# build context is .docker ...

# ... so the following COPY refers to .docker/.shared
COPY ./.shared /tmp

# ... so the following COPY refers to .docker/nginx/nginx.conf
COPY ./nginx/nginx.conf /tmp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build context for all of our containers will be the &lt;code&gt;.docker&lt;/code&gt; directory,  so that all build processes have access to the &lt;code&gt;.shared&lt;/code&gt; scripts and config.  Yes, that also means that the &lt;code&gt;php-fpm&lt;/code&gt; container has access to files that are only relevant to the &lt;code&gt;mysql&lt;/code&gt; container (for instance), but the performance penalty is absolutely neglectable. Plus, as long as we don't actively &lt;code&gt;COPY&lt;/code&gt; those irrelevant files, they won't bloat up our images.&lt;/p&gt;

&lt;p&gt;A couple of notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I used to think that the build context is &lt;em&gt;always&lt;/em&gt; tied to the location of the Dockerfile but that's only the default,
it can be any directory&lt;/li&gt;
&lt;li&gt;the build context is &lt;strong&gt;actually send&lt;/strong&gt; to the build process - i.e. you should avoid unnecessary files / folders as this might
affect performance, especially on big files (iaw: don't use &lt;code&gt;/&lt;/code&gt; as context!)&lt;/li&gt;
&lt;li&gt;similar to &lt;code&gt;git&lt;/code&gt;, Docker knows the concept of a &lt;a href="https://docs.docker.com/engine/reference/builder/#dockerignore-file" rel="noopener noreferrer"&gt;&lt;code&gt;.dockerignore&lt;/code&gt; file&lt;/a&gt;
to exclude files from being included in the build context&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a id="dockerfile-template"&gt;&lt;/a&gt;Dockerfile template
&lt;/h3&gt;

&lt;p&gt;The Dockerfiles for the containers roughly follow the structure outlined below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ...

# path to the directory where the Dockerfile lives relative to the build context
ARG SERVICE_DIR="./service"

# get the scripts from the build context and make sure they are executable
COPY .shared/scripts/ /tmp/scripts/
RUN chmod +x -R /tmp/scripts/

# set timezone
ARG TZ=UTC
RUN /tmp/scripts/set_timezone.sh ${TZ}

# add users
ARG APP_USER=www-data
ARG APP_USER_ID=1000
ARG APP_GROUP=$(APP_USER)
ARG APP_GROUP_ID=$(APP_USER_ID)

RUN /tmp/scripts/create_user.sh ${APP_USER} ${APP_GROUP} ${APP_USER_ID} ${APP_GROUP_ID}

# install common software
RUN /tmp/scripts/install_software.sh

# perform any other, container specific build steps
COPY ${SERVICE_DIR}/config/* /etc/service/config
RUN /tmp/scripts/modify_config.sh /etc/service/config/default.conf \
    "__APP_USER" \
    "${APP_USER}" \
;
# [...]

# set default work directory
WORKDIR "..."

# cleanup 
RUN /tmp/scripts/cleanup.sh

# define ENTRYPOINT
ENTRYPOINT [...]
CMD [...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comments should suffice to give you an overview - so let's talk about the individual parts in detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="setting-the-timezone"&gt;&lt;/a&gt;Setting the timezone
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;set_timezone.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's start with a simple and obvious one: Ensuring that all containers use the same system timezone (see &lt;a href="https://www.itzgeek.com/how-tos/linux/debian/how-to-change-timezone-in-debian-9-8-ubuntu-16-04-14-04-linuxmint-18.html" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://unix.stackexchange.com/q/452559" rel="noopener noreferrer"&gt;here&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

TZ=$1
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &amp;amp;&amp;amp; echo $TZ &amp;gt; /etc/timezone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is then called from the &lt;code&gt;Dockerfile&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG TZ=UTC
RUN /tmp/scripts/set_timezone.sh ${TZ}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="synchronizing-file-and-folder-ownership-on-shared-volumes"&gt;&lt;/a&gt;Synchronizing file and folder ownership on shared volumes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;create_user.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker makes it really easy to share files between containers by using &lt;a href="https://docs.docker.com/storage/volumes/" rel="noopener noreferrer"&gt;volumes&lt;/a&gt;.  For simplicities sake, you can picture a volume simply as an additional disk that multiple containers have access to. And since it's PHP we're talking about here, sharing the same application files is a common requirement  (e.g. for &lt;code&gt;php-fpm&lt;/code&gt;, &lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;php-workers&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;As long as you are only dealing with one container, life is easy: You can simply &lt;code&gt;chown&lt;/code&gt; files to the correct user. But since the containers might have a different user setup, permissions/ownership becomes a problem. Checkout  &lt;a href="https://serversforhackers.com/c/dckr-file-permissions" rel="noopener noreferrer"&gt;this video on Docker &amp;amp; File Permissions&lt;/a&gt; for a practical  example in a Laravel application.&lt;/p&gt;

&lt;p&gt;The first thing for me was understanding that file ownership does not depend on the user &lt;strong&gt;name&lt;/strong&gt; but rather on the user &lt;strong&gt;id&lt;/strong&gt;.  And you might have guessed it: Two containers might have a user with the same name but with a different id.  The same is true for groups, btw. You can check the id by running &lt;code&gt;id &amp;lt;name&amp;gt;&lt;/code&gt;, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/structuring-the-docker-setup-for-php-projects/docker-file-ownership-volume.png" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fstructuring-the-docker-setup-for-php-projects%2Fdocker-file-ownership-volume.png" alt="File ownership with multiple containers using a shared volume"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's inconvenient but rather easy to solve in most cases, because we have full control over the containers and can &lt;a href="https://www.cyberciti.biz/faq/linux-change-user-group-uid-gid-for-all-owned-files/" rel="noopener noreferrer"&gt;assign ids as we like&lt;/a&gt;  (using &lt;code&gt;usermod -u &amp;lt;id&amp;gt; &amp;lt;name&amp;gt;&lt;/code&gt;) and thus making sure every container uses the same user names with the same user ids.&lt;/p&gt;

&lt;p&gt;Things get complicated when the volume isn't just a Docker volume but a shared folder on the host. This is usually what we want for development, so that changes on the host are immediately reflected in all the containers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/structuring-the-docker-setup-for-php-projects/docker-file-ownership-host.png" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fstructuring-the-docker-setup-for-php-projects%2Fdocker-file-ownership-host.png" alt="File ownership with multiple containers using a shared volume from the host"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This issue &lt;strong&gt;only affects users with a linux host system&lt;/strong&gt;! Docker Desktop (previously known as Docker for Mac / Docker for Win) has a virtualization layer in between that will effectively erase all ownership settings and make everything shared from the host available to every user in a container.&lt;/p&gt;

&lt;p&gt;We use the following script to ensure a consistent user setup when building a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

APP_USER=$1
APP_GROUP=$2
APP_USER_ID=$3
APP_GROUP_ID=$4

new_user_id_exists=$(id ${APP_USER_ID} &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; echo $?) 
if [ "$new_user_id_exists" = "0" ]; then
    (&amp;gt;&amp;amp;2 echo "ERROR: APP_USER_ID $APP_USER_ID already exists - Aborting!");
    exit 1;
fi

new_group_id_exists=$(getent group ${APP_GROUP_ID} &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; echo $?) 
if [ "$new_group_id_exists" = "0" ]; then
    (&amp;gt;&amp;amp;2 echo "ERROR: APP_GROUP_ID $APP_GROUP_ID already exists - Aborting!");
    exit 1;
fi

old_user_id=$(id -u ${APP_USER})
old_user_exists=$(id -u ${APP_USER} &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; echo $?) 
old_group_id=$(getent group ${APP_GROUP} | cut -d: -f3)
old_group_exists=$(getent group ${APP_GROUP} &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; echo $?)

if [ "$old_group_id" != "${APP_GROUP_ID}" ]; then
    # create the group
    groupadd -f ${APP_GROUP}
    # and the correct id
    groupmod -g ${APP_GROUP_ID} ${APP_GROUP}
    if [ "$old_group_exists" = "0" ]; then
        # set the permissions of all "old" files and folder to the new group
        find / -group $old_group_id -exec chgrp -h ${APP_GROUP} {} \; || true
    fi 
fi

if [ "$old_user_id" != "${APP_USER_ID}" ]; then
    # create the user if it does not exist
    if [ "$old_user_exists" != "0" ]; then
        useradd ${APP_USER} -g ${APP_GROUP}
    fi

    # make sure the home directory exists with the correct permissions
    mkdir -p /home/${APP_USER} &amp;amp;&amp;amp; chmod 755 /home/${APP_USER} &amp;amp;&amp;amp; chown ${APP_USER}:${APP_GROUP} /home/${APP_USER} 

    # change the user id, set the home directory and make sure the user has a login shell
    usermod -u ${APP_USER_ID} -m -d /home/${APP_USER} ${APP_USER} -s $(which bash)

    if [ "$old_user_exists" = "0" ]; then
        # set the permissions of all "old" files and folder to the new user 
        find / -user $old_user_id -exec chown -h ${APP_USER} {} \; || true
    fi
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is then called from the &lt;code&gt;Dockerfile&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG APP_USER=www-data
ARG APP_USER_ID=1000
ARG APP_GROUP=$(APP_USER)
ARG APP_GROUP_ID=$(APP_USER_ID)

RUN /tmp/scripts/create_user.sh ${APP_USER} ${APP_GROUP} ${APP_USER_ID} ${APP_GROUP_ID}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default values can be overridden by passing in the corresponding  &lt;a href="https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg" rel="noopener noreferrer"&gt;build args&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Linux users should use the user id of the user on their host system - for Docker Desktop users the defaults are fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="modifying-configuration-files"&gt;&lt;/a&gt;Modifying configuration files
&lt;/h3&gt;

&lt;p&gt;For most services we probably need some custom configuration settings, like&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setting php.ini values&lt;/li&gt;
&lt;li&gt;changing the default user of a service &lt;/li&gt;
&lt;li&gt;changing the location of logfiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a couple of   &lt;a href="https://dantehranian.wordpress.com/2015/03/25/how-should-i-get-application-configuration-into-my-docker-containers/" rel="noopener noreferrer"&gt;common approaches to modify application configuration in docker&lt;/a&gt; and we are currently trying to stick to two rules: 1. provide additional files that override defaults if possible 2. change non-static values with a simple search and replace via &lt;code&gt;sed&lt;/code&gt; during the container build&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a id="providing-additional-config-files"&gt;&lt;/a&gt;Providing additional config files
&lt;/h4&gt;

&lt;p&gt;Most services allow the specification of additional configuration files that override the default values in  a default config file. This is great because we only need to define the settings that we actually care about  instead of copying a full file with lots of redundant values.&lt;/p&gt;

&lt;p&gt;Take the &lt;a href="http://php.net/manual/en/configuration.file.php#configuration.file.scan" rel="noopener noreferrer"&gt;&lt;code&gt;php.ini&lt;/code&gt; file&lt;/a&gt; for example: It allows to places additional &lt;code&gt;.ini&lt;/code&gt; files in a specific directory that override the default values. An easy way to find this directory is &lt;code&gt;php -i | grep "additional .ini"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ php -i | grep "additional .ini"
Scan this dir for additional .ini files =&amp;gt; /usr/local/etc/php/conf.d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead of providing a "full" &lt;code&gt;php.ini&lt;/code&gt; file, we will use a &lt;code&gt;zz-app.ini&lt;/code&gt; file instead, that &lt;strong&gt;only&lt;/strong&gt; contains the .ini settings we actually want to change and place it under &lt;code&gt;/usr/local/etc/php/conf.d&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;zz-&lt;/code&gt;? Because&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[...] Within each directory, PHP will scan all files ending in .ini in alphabetical order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;so if we want to ensure that our .ini files comes last (overriding all previous settings), we'll give it a corresponding prefix :)&lt;/p&gt;

&lt;p&gt;The full process would look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;place the file in the &lt;code&gt;.docker&lt;/code&gt; folder, e.g. at &lt;code&gt;.docker/.shared/config/php/conf.d/zz-app.ini&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pass the folder as build context&lt;/li&gt;
&lt;li&gt;in the Dockerfile, use &lt;code&gt;COPY .shared/config/php/conf.d/zz-app.ini /usr/local/etc/php/conf.d/zz-app.ini&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;a id="changing-non-static-values"&gt;&lt;/a&gt;Changing non-static values
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;modify_config.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some configuration values are subject to local settings and thus should not be hard coded in configuration files.  Take the &lt;code&gt;memory_limit&lt;/code&gt; configuration for &lt;code&gt;php-fpm&lt;/code&gt; as an example: Maybe someone in the team can only dedicate a limited amount of memory to docker, so the &lt;code&gt;memory_limit&lt;/code&gt; has to be kept lower than usual.&lt;/p&gt;

&lt;p&gt;We'll account for that fact by using a variable prefixed by &lt;code&gt;__&lt;/code&gt; instead of the real value and replace it with a dynamic argument in the Dockerfile. Example for the aforementioned &lt;code&gt;zz-app.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memory_limit = __MEMORY_LIMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the following script &lt;code&gt;modify_config.sh&lt;/code&gt; to replace the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

CONFIG_FILE=$1
VAR_NAME=$2
VAR_VALUE=$3

sed -i -e "s#${VAR_NAME}#${VAR_VALUE}#" "${CONFIG_FILE}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is then called from the &lt;code&gt;Dockerfile&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG PHP_FPM_MEMORY_LIMIT=1024M

RUN /tmp/scripts/modify_config.sh \
    "/usr/local/etc/php/conf.d/zz-app.ini" \
    "__MEMORY_LIMIT" \
    "${PHP_FPM_MEMORY_LIMIT}" \
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;PHP_FPM_MEMORY_LIMIT&lt;/code&gt; has a default value of &lt;code&gt;1024M&lt;/code&gt; but can be overridden when the actual build is initiated.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="installing-php-extensions"&gt;&lt;/a&gt;Installing php extensions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;install_php_extensions.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When php extensions are missing, googling will often point to answers for normal linux systems using &lt;code&gt;apt-get&lt;/code&gt; or &lt;code&gt;yum&lt;/code&gt;,  e.g. &lt;code&gt;sudo apt-get install php-xdebug&lt;/code&gt;. But for the official docker images, the recommended way is using the  &lt;a href="https://github.com/docker-library/docs/blob/master/php/README.md#how-to-install-more-php-extensions" rel="noopener noreferrer"&gt;docker-php-ext-configure, docker-php-ext-install, and docker-php-ext-enable helper scripts&lt;/a&gt;. Unfortunately, some extensions have rather complicated dependencies, so that the installation fails. Fortunately, there is a great project on Github called  &lt;a href="https://github.com/mlocati/docker-php-extension-installer" rel="noopener noreferrer"&gt;docker-php-extension-installer&lt;/a&gt; that takes care of that for us and is super easy to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM php:7.3-cli

ADD https://raw.githubusercontent.com/mlocati/docker-php-extension-installer/master/install-php-extensions /usr/local/bin/

RUN chmod uga+x /usr/local/bin/install-php-extensions &amp;amp;&amp;amp; sync &amp;amp;&amp;amp; install-php-extensions xdebug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The readme also contains an  &lt;a href="https://github.com/mlocati/docker-php-extension-installer#supported-php-extensions" rel="noopener noreferrer"&gt;overview of supported extension&lt;/a&gt;  per PHP version. To ensure that all of our PHP containers have the same extensions, we provide the following script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

# add wget
apt-get update -yqq &amp;amp;&amp;amp; apt-get -f install -yyq wget

# download helper script
wget -q -O /usr/local/bin/install-php-extensions https://raw.githubusercontent.com/mlocati/docker-php-extension-installer/master/install-php-extensions \
    || (echo "Failed while downloading php extension installer!"; exit 1)

# install all required extensions
chmod uga+x /usr/local/bin/install-php-extensions &amp;amp;&amp;amp; sync &amp;amp;&amp;amp; install-php-extensions \
    xdebug \
    opcache \
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're not sure which extensions are required by your application, give the  &lt;a href="https://github.com/maglnet/ComposerRequireChecker" rel="noopener noreferrer"&gt;ComposerRequireChecker&lt;/a&gt; a try.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a id="installing-common-software"&gt;&lt;/a&gt;Installing common software
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;install_software.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is a certain set of software that I want to have readily available in every container. Since this a development  setup, I'd prioritize ease of use / debug over performance / image size, so this might seem like a little "too much". I think I'm also kinda spoiled by my Homestead past, because it's so damn convenient to have everything right at your fingertips :)&lt;/p&gt;

&lt;p&gt;Anyway, the script is straight forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

apt-get update -yqq &amp;amp;&amp;amp; apt-get install -yqq \
    curl \
    dnsutils \
    gdb \
    git \
    htop \
    iputils-ping \
    iproute2 \
    ltrace \
    make \
    procps \
    strace \
    sudo \
    sysstat \
    unzip \
    vim \
    wget \
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this list should match &lt;strong&gt;your own set of go-to tools&lt;/strong&gt;. I'm fairly open to adding new stuff here if it speeds up the
dev workflow. But if you don't require some of the tools, get rid of them.&lt;/li&gt;
&lt;li&gt;sorting the software alphabetically is a good practice to avoid unnecessary duplicates. Don't do this by hand, though!
If you're using an IDE / established text editor, chances are high that this is either a build-in functionality or
there's a plugin available. I'm using &lt;a href="https://plugins.jetbrains.com/plugin/5919-lines-sorter" rel="noopener noreferrer"&gt;Lines Sorter for PhpStorm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a id="cleaning-up"&gt;&lt;/a&gt;Cleaning up
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;cleanup.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nice and simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

apt-get clean
rm -rf /var/lib/apt/lists/* \
       /tmp/* \
       /var/tmp/* \
       /var/log/lastlog \
       /var/log/faillog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a id="using-entrypoint-for-pre-run-configuration"&gt;&lt;/a&gt;Using &lt;code&gt;ENTRYPOINT&lt;/code&gt; for pre-run configuration
&lt;/h3&gt;

&lt;p&gt;Docker went back to the unix roots with the  &lt;a href="https://en.wikipedia.org/wiki/Unix_philosophy#Do_One_Thing_and_Do_It_Well" rel="noopener noreferrer"&gt;do on thing and do it well philosophy&lt;/a&gt; which is  manifested in the &lt;a href="https://medium.freecodecamp.org/docker-entrypoint-cmd-dockerfile-best-practices-abc591c30e21" rel="noopener noreferrer"&gt;&lt;code&gt;CMD&lt;/code&gt; and &lt;code&gt;ENTRYPOINT&lt;/code&gt; instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As I had a hard time understanding those instructions when I started with Docker, here's my take at a layman's terms description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;since a container should do one thing, we need to specify that thing. That's what we do with &lt;code&gt;ENTRYPOINT&lt;/code&gt;. Concrete examples:

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;mysql&lt;/code&gt; container should probably run the &lt;code&gt;mysqld&lt;/code&gt; daemon&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;php-fpm&lt;/code&gt; container.. well, &lt;code&gt;php-fpm&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;the &lt;code&gt;CMD&lt;/code&gt; is passed as the default argument to the &lt;code&gt;ENTRYPOINT&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;the &lt;code&gt;ENTRYPOINT&lt;/code&gt; is executed every time we &lt;em&gt;run&lt;/em&gt; a container. Some things can't be done during build but only at runtime
(e.g. find the IP of the host from within a container - see section 
&lt;a href="https://dev.toproviding-host-docker-internal-for-linux-host-systems"&gt;Providing &lt;code&gt;host.docker.internal&lt;/code&gt; for linux host systems&lt;/a&gt;
) - &lt;code&gt;ENTRYPOINT&lt;/code&gt; is a good solution for that problem&lt;/li&gt;

&lt;li&gt;technically, we can only override an already existing &lt;code&gt;ENTRYPOINT&lt;/code&gt; from the base image. But: We can structure the new 
&lt;code&gt;ENTRYPOINT&lt;/code&gt; like a &lt;a href="https://en.wikipedia.org/wiki/Decorator_pattern" rel="noopener noreferrer"&gt;decorator&lt;/a&gt; by adding &lt;code&gt;exec "$@"&lt;/code&gt; at the end to 
simulate inheritance from the parent image&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;To expand on the last point, consider the default  &lt;a href="https://github.com/docker-library/php/blob/640a30e8ff27b1ad7523a212522472fda84d56ff/7.3/stretch/fpm/docker-php-entrypoint" rel="noopener noreferrer"&gt;&lt;code&gt;ENTRYPOINT&lt;/code&gt; of the current [2019-02-23; PHP 7.3] &lt;code&gt;php-fpm&lt;/code&gt; image&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

set -e

# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then
    set -- php-fpm "$@"
fi

exec "$@"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 In the &lt;a href="https://github.com/docker-library/php/blob/640a30e8ff27b1ad7523a212522472fda84d56ff/7.3/stretch/fpm/Dockerfile#L223" rel="noopener noreferrer"&gt;corresponding Dockerfile&lt;/a&gt; we find the following instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# [...]
ENTRYPOINT ["docker-php-entrypoint"]
# [...]
CMD ["php-fpm"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 That means: When we run the container it will pass the string "php-fpm" to the &lt;code&gt;ENTRYPOINT&lt;/code&gt; script &lt;code&gt;docker-php-entrypoint&lt;/code&gt;  as argument which will then execute it (due to the &lt;code&gt;exec "$@"&lt;/code&gt; instruction at the end):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run --name test --rm php:fpm
[23-Feb-2019 14:49:20] NOTICE: fpm is running, pid 1
[23-Feb-2019 14:49:20] NOTICE: ready to handle connections
# php-fpm is running
# Hit ctrl + c to close the connection
$ docker stop test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could now override the default &lt;code&gt;CMD&lt;/code&gt; "php-fpm" with something else, e.g. a simple &lt;code&gt;echo "hello"&lt;/code&gt;. The &lt;code&gt;ENTRYPOINT&lt;/code&gt; will happily execute it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run --name test --rm php:fpm echo "hello"
hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 But now the &lt;code&gt;php-fpm&lt;/code&gt; process isn't started any more. How can we echo "hello" but still keep the fpm process running?&lt;br&gt;
By adding our own &lt;code&gt;ENTRYPOINT&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh
echo 'hello'

exec "$@"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Full example (using &lt;a href="https://docs.docker.com/engine/reference/commandline/build/#build-with--" rel="noopener noreferrer"&gt;stdin to pass the Dockerfile&lt;/a&gt;  via &lt;a href="https://stackoverflow.com/q/2953081/413531" rel="noopener noreferrer"&gt;Heredoc string&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker build -t my-fpm -&amp;lt;&amp;lt;'EOF'
FROM php:fpm

RUN  touch "/usr/bin/my-entrypoint.sh" \
  &amp;amp;&amp;amp; echo "#!/bin/sh" &amp;gt;&amp;gt; "/usr/bin/my-entrypoint.sh" \
  &amp;amp;&amp;amp; echo "echo 'hello'" &amp;gt;&amp;gt; "/usr/bin/my-entrypoint.sh" \
  &amp;amp;&amp;amp; echo "exec \"\$@\"" &amp;gt;&amp;gt; "/usr/bin/my-entrypoint.sh" \
  &amp;amp;&amp;amp; chmod +x "/usr/bin/my-entrypoint.sh" \
  &amp;amp;&amp;amp; cat "/usr/bin/my-entrypoint.sh" \
;

ENTRYPOINT ["/usr/bin/my-entrypoint.sh", "docker-php-entrypoint"]
CMD ["php-fpm"]
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Note that we added the &lt;code&gt;ENTRYPOINT&lt;/code&gt; of the parent image &lt;code&gt;docker-php-entrypoint&lt;/code&gt; as argument to our own &lt;code&gt;ENTRYPOINT&lt;/code&gt; script &lt;code&gt;/usr/bin/my-entrypoint.sh&lt;/code&gt; so that we don't loose its functionality. And we need to define the &lt;code&gt;CMD&lt;/code&gt; instruction explicitly, because the one from the parent image is &lt;a href="https://stackoverflow.com/a/49031590/413531" rel="noopener noreferrer"&gt;automatically removed once we define our own &lt;code&gt;ENTRYPOINT&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But: It works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run --name test --rm my-fpm
hello
[23-Feb-2019 15:43:25] NOTICE: fpm is running, pid 1
[23-Feb-2019 15:43:25] NOTICE: ready to handle connections
# Hit ctrl + c to close the connection
$ docker stop test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a id="providing-host-docker-internal-for-linux-host-systems"&gt;&lt;/a&gt;Providing &lt;code&gt;host.docker.internal&lt;/code&gt; for linux host systems
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Script: &lt;code&gt;docker-entrypoint/resolve-docker-host-ip.sh&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the previous part of this tutorial series, I explained how to build the  &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker" rel="noopener noreferrer"&gt;Docker container in a way that it plays nice with PhpStorm and Xdebug&lt;/a&gt;.  The key parts were SSH access and the magical &lt;code&gt;host.docker.internal&lt;/code&gt; DNS entry. This works great for Docker Desktop (Windows and Mac) but not for Linux. The DNS entry &lt;a href="https://github.com/docker/for-linux/issues/264" rel="noopener noreferrer"&gt;doesn't exist there&lt;/a&gt;.  Since we rely on that entry  &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/#fix-xdebug-on-phpstorm-when-run-from-a-docker-container" rel="noopener noreferrer"&gt;to make debugging possible&lt;/a&gt;, we will set it "manually" &lt;a href="https://stackoverflow.com/a/24049165/413531" rel="noopener noreferrer"&gt;if the host doesn't exist&lt;/a&gt;  with the following script  (inspired by the article &lt;a href="https://dev.to/bufferings/access-host-from-a-docker-container-4099"&gt;Access host from a docker container&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

set -e

HOST_DOMAIN="host.docker.internal"

# check if the host exists - this will fail on linux
if dig ${HOST_DOMAIN} | grep -q 'NXDOMAIN'
then
  # resolve the host IP
  HOST_IP=$(ip route | awk 'NR==1 {print $3}')
  # and write it to the hosts file
  echo "$HOST_IP\t$HOST_DOMAIN" &amp;gt;&amp;gt; /etc/hosts
fi

exec "$@"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is placed at &lt;code&gt;.shared/docker-entrypoint/resolve-docker-host-ip.sh&lt;/code&gt; and added as &lt;code&gt;ENTRYPOINT&lt;/code&gt; in the Dockerfile via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COPY .shared/scripts/ /tmp/scripts/

RUN mkdir -p /bin/docker-entrypoint/ \
 &amp;amp;&amp;amp; cp /tmp/scripts/docker-entrypoint/* /bin/docker-entrypoint/ \
 &amp;amp;&amp;amp; chmod +x -R /bin/docker-entrypoint/ \
;

ENTRYPOINT ["/bin/docker-entrypoint/resolve-docker-host-ip.sh", ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;since this script depends on runtime configuration, we need to run it as an &lt;code&gt;ENTRYPOINT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;there is no need to explicitly check for the OS type - we simply make sure that the DNS entry exists
and add it if it doesn't&lt;/li&gt;
&lt;li&gt;we're using &lt;code&gt;dig&lt;/code&gt; (package &lt;code&gt;dnsutils&lt;/code&gt;) and &lt;code&gt;ip&lt;/code&gt; (package &lt;code&gt;iproute2&lt;/code&gt;) which need to be installed 
during the build time of the container. Tip: If you need to figure out the package for a specific command,
give &lt;a href="https://command-not-found.com/" rel="noopener noreferrer"&gt;https://command-not-found.com/&lt;/a&gt; a try. See the 
&lt;a href="https://command-not-found.com/dig" rel="noopener noreferrer"&gt;entry for &lt;code&gt;dig&lt;/code&gt;&lt;/a&gt; for instance.&lt;/li&gt;
&lt;li&gt;this workaround is only required in containers we want to debug via xdebug&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a id="wrapping-up"&gt;&lt;/a&gt;Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. Apart from that, you should now have a running docker setup for your local PHP development as well as a nice "flow" to get started each day.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;add some more containers (php workers, mysql, redis)&lt;/a&gt; and use &lt;a href="https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/" rel="noopener noreferrer"&gt;a fresh installation of Laravel 9&lt;/a&gt; to make use of them.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>php</category>
      <category>docker</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>PhpStorm, Docker and Xdebug 3 on PHP 8.1 in 2022 [Tutorial Part 3]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Fri, 24 Jun 2022 08:22:02 +0000</pubDate>
      <link>https://dev.to/pascallandau/phpstorm-docker-and-xdebug-3-on-php-81-in-2022-tutorial-part-42-37m7</link>
      <guid>https://dev.to/pascallandau/phpstorm-docker-and-xdebug-3-on-php-81-in-2022-tutorial-part-42-37m7</guid>
      <description>&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/" rel="noopener noreferrer"&gt;PhpStorm, Docker and Xdebug 3 on PHP 8.1 in 2022 [Tutorial Part 3]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In this part of the tutorial series on developing PHP on Docker we  will &lt;strong&gt;setup our local development environment to be used by PhpStorm and Xdebug&lt;/strong&gt;. We will also  ensure that we can run &lt;strong&gt;PHPUnit tests from the command line as well as from PhpStorm&lt;/strong&gt; and throw  the tool &lt;code&gt;strace&lt;/code&gt; into the mix for debugging long running processes.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/bZ1MiynqT98"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;All code samples are publicly available in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;. You find the branch for this tutorial at  &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-4-2-phpstorm-docker-xdebug-3-php-8-1-in-2022" rel="noopener noreferrer"&gt;part-4-2-phpstorm-docker-xdebug-3-php-8-1-in-2022&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;Docker from scratch for PHP 8.1 Applications in 2022&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/" rel="noopener noreferrer"&gt;Run Laravel 9 on Docker in 2022&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;
Install Tools

&lt;ul&gt;
&lt;li&gt;Install composer&lt;/li&gt;
&lt;li&gt;Install Xdebug&lt;/li&gt;
&lt;li&gt;Install PHPUnit&lt;/li&gt;
&lt;li&gt;Install SSH&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Setup PhpStorm

&lt;ul&gt;
&lt;li&gt;SSH Configuration&lt;/li&gt;
&lt;li&gt;PHP Interpreter&lt;/li&gt;
&lt;li&gt;PHPUnit&lt;/li&gt;
&lt;li&gt;
Debugging

&lt;ul&gt;
&lt;li&gt;Debug code executed via PhpStorm&lt;/li&gt;
&lt;li&gt;
Debug code executed via php-fpm, cli or from a worker

&lt;ul&gt;
&lt;li&gt;php-fpm&lt;/li&gt;
&lt;li&gt;cli&lt;/li&gt;
&lt;li&gt;php-workers&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;strace&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This article is mostly an update of &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/" rel="noopener noreferrer"&gt;Setting up PhpStorm with Xdebug for local development on Docker&lt;/a&gt; but will also cover the "remaining cases" of &lt;strong&gt;debugging php-fpm&lt;/strong&gt; and &lt;strong&gt;php worker processes&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;We will still rely on an &lt;strong&gt;always-running docker setup&lt;/strong&gt; that we connect to via an SSH Configuration instead of using the  &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/#run-debug-a-php-script-on-docker-server" rel="noopener noreferrer"&gt;built-in docker-compose capabilities&lt;/a&gt; as I feel it's closer to what we do in CI / production. However, we will &lt;strong&gt;not use SSH keys&lt;/strong&gt;  any longer but simply authenticate via password. This reduces complexity and removes any  pesky warnings regarding "SSH keys being exposed in a repository".&lt;/p&gt;

&lt;p&gt;&lt;a id="install-tools"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Tools
&lt;/h2&gt;

&lt;p&gt;&lt;a id="install-composer"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Install composer &lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; is installed by pulling  &lt;a href="https://hub.docker.com/_/composer" rel="noopener noreferrer"&gt;the official composer docker image&lt;/a&gt; and simply "copying" the  composer executable over to the base php image. In addition, composer needs the extensions &lt;code&gt;mbstring&lt;/code&gt; and &lt;code&gt;phar&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# File: .docker/images/php/base/Dockerfile

ARG ALPINE_VERSION
ARG COMPOSER_VERSION
FROM composer:${COMPOSER_VERSION} as composer
FROM alpine:${ALPINE_VERSION} as base

# ...

RUN apk add --update --no-cache  \
        php-mbstring~=${TARGET_PHP_VERSION} \
        php-phar~=${TARGET_PHP_VERSION} \

# ...

COPY --from=composer /usr/bin/composer /usr/local/bin/composer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we want our build to be deterministic, we "pin" the composer version by adding a  &lt;code&gt;COMPOSER_VERSION&lt;/code&gt; variable to the &lt;code&gt;.docker/.env&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPOSER_VERSION=2.2.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and using it in &lt;code&gt;.docker/docker-compose/docker-compose-php-base.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;php-base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;COMPOSER_VERSION=${COMPOSER_VERSION?}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="install-xdebug"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Xdebug Install the extension via &lt;code&gt;apk&lt;/code&gt; (only for the &lt;code&gt;local&lt;/code&gt; target):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: .docker/images/php/base/Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        php-xdebug~&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET_PHP_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="c"&gt;# ensure that xdebug is not enabled by default&lt;/span&gt;
    &amp;amp;&amp;amp; rm -f /etc/php8/conf.d/00_xdebug.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also don't want to enable &lt;code&gt;xdebug&lt;/code&gt; immediately but only when we need it (due to the decrease  in performance when the extension is enabled), hence we remove the default config file and  disable the extension in the application &lt;code&gt;.ini&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# File: .docker/images/php/base/conf.d/zz-app-local.ini

; Note:
; Remove the comment ; to enable debugging
;zend_extension=xdebug
xdebug.client_host=host.docker.internal
xdebug.start_with_request=yes
xdebug.mode=debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/#fix-xdebug-on-phpstorm-when-run-from-a-docker" rel="noopener noreferrer"&gt;Fix Xdebug on PhpStorm when run from a Docker container&lt;/a&gt; for an explanation of the &lt;code&gt;xdebug.client_host=host.docker.internal&lt;/code&gt; setting (previously called &lt;code&gt;xdebug.remote_host&lt;/code&gt; in xdebug &amp;lt; 3). This will still work out of the box for Docker Desktop, but for Linux users we need to add the  &lt;a href="https://github.com/docker/for-linux/issues/264#issuecomment-965465879" rel="noopener noreferrer"&gt;&lt;code&gt;host-gateway&lt;/code&gt; magic reference&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;to all PHP containers&lt;/strong&gt; (we can't add it to the php base image because this is a runtime setting):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;host.docker.internal:host-gateway&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we need to add  &lt;a href="https://www.jetbrains.com/help/phpstorm/debugging-a-php-cli-script.html" rel="noopener noreferrer"&gt;the environment variable &lt;code&gt;PHP_IDE_CONFIG&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;to all PHP containers&lt;/strong&gt;. The variable is defined as &lt;code&gt;PHP_IDE_CONFIG=serverName=dofroscra&lt;/code&gt;, where  "dofroscra" is the name of the server that we will configure later for debugging. Because we  need the same value in multiple places, the variable is configured in &lt;code&gt;.docker/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHP_IDE_CONFIG=serverName=dofroscra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then added in &lt;code&gt;.docker/docker-compose/docker-compose.local.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;php-fpm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}&lt;/span&gt;

  &lt;span class="na"&gt;php-worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}&lt;/span&gt;

  &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="install-phpunit"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Install PHPUnit PHPUnit will be installed via &lt;code&gt;composer&lt;/code&gt; but will not be "baked into the image" for local  development. Thus, we must run &lt;code&gt;composer require&lt;/code&gt; &lt;strong&gt;in the container&lt;/strong&gt;. To make this more  convenient a make target for running arbitrary composer commands is added in  &lt;code&gt;.make/01-00-application-setup.mk&lt;/code&gt;:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;composer&lt;/span&gt;
&lt;span class="nl"&gt;composer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run composer commands. Specify the command e.g. via ARGS="install"&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; composer &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows me to run &lt;code&gt;make composer ARGS="install"&lt;/code&gt; from the host system to execute &lt;code&gt;composer  install&lt;/code&gt; in the container. In consequence, &lt;code&gt;composer&lt;/code&gt; will use the PHP version and extensions of  the &lt;code&gt;application&lt;/code&gt; container to install the dependencies, yet I will still see the installed files locally because the codebase is configured as a volume for the container.&lt;/p&gt;

&lt;p&gt;Before installing phpunit, we must add the required extensions &lt;code&gt;dom&lt;/code&gt; and &lt;code&gt;xml&lt;/code&gt; to the container&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# File: .docker/images/php/base/Dockerfile

# ...

RUN apk add --update --no-cache  \
        php-dom~=${TARGET_PHP_VERSION} \
        php-xml~=${TARGET_PHP_VERSION} \
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as well as rebuild and restart the docker setup via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make docker-build
make docker-down
make docker-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can add phpunit via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make composer ARGS='require "phpunit/phpunit"'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which will create a &lt;code&gt;composer.json&lt;/code&gt; file and setup up the &lt;code&gt;vendor/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make composer ARGS='require "phpunit/phpunit"'
Using version ^9.5 for phpunit/phpunit
./composer.json has been created
Running composer update phpunit/phpunit
Loading composer repositories with package information
Updating dependencies
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CAUTION&lt;/strong&gt;: If you &lt;strong&gt;run into the following permission error&lt;/strong&gt; at this step, you are likely using  Linux and  haven't set the &lt;code&gt;APP_USER_ID&lt;/code&gt; and &lt;code&gt;APP_GROUP_ID&lt;/code&gt; variables as described in the previous article under  &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#solving-permission-issues" rel="noopener noreferrer"&gt;Solving permission issues&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make composer ARGS='req phpunit/phpunit' ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application application composer req phpunit/phpunit
./composer.json is not writable.
make: *** [.make/01-00-application-setup.mk:14: composer] Error 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have also added &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a minimal &lt;code&gt;phpunit.xml&lt;/code&gt; config file&lt;/li&gt;
&lt;li&gt;a test case at &lt;code&gt;tests/SomeTest.php&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;and a new Makefile for "anything related to qa" at &lt;code&gt;.make/01-02-application-qa.mk&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;##@ [Application: QA]
&lt;/span&gt;
&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;
&lt;span class="nl"&gt;test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run the test suite &lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_WORKER_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; vendor/bin/phpunit &lt;span class="nt"&gt;-c&lt;/span&gt; phpunit.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I can run tests simply via &lt;code&gt;make test&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make test
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker vendor/bin/phpunit
PHPUnit 9.5.13 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.324, Memory: 4.00 MB

OK (1 test, 1 assertion)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="install-ssh"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Install SSH
&lt;/h3&gt;

&lt;p&gt;We will execute commands from PhpStorm via ssh in the &lt;code&gt;application&lt;/code&gt; container. As mentioned, we  won't use a key file for authentication but will instead simply use a password that is  configured via the &lt;code&gt;APP_SSH_PASSWORD&lt;/code&gt; variable in &lt;code&gt;.docker/.env&lt;/code&gt; and passed to the image in  &lt;code&gt;.docker/docker-compose/docker-compose.local.yml&lt;/code&gt;. In addition, we map port &lt;code&gt;2222&lt;/code&gt; from the  host system to port &lt;code&gt;22&lt;/code&gt; of the application container and make sure that the codebase is shared as a volume between host and container&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_SSH_PASSWORD=${APP_SSH_PASSWORD?}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${APP_CODE_PATH_HOST?}:${APP_CODE_PATH_CONTAINER?}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${APPLICATION_SSH_HOST_PORT:-2222}:22"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container already contains &lt;code&gt;openssh&lt;/code&gt; and sets the password&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BASE_IMAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;${BASE_IMAGE}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        openssh

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP_SSH_PASSWORD&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_USER_NAME&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$APP_SSH_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | chpasswd 2&amp;gt;&amp;amp;1

&lt;span class="c"&gt;# Required to start sshd, otherwise the container will error out on startup with the message&lt;/span&gt;
&lt;span class="c"&gt;# "sshd: no hostkeys available -- exiting."&lt;/span&gt;
&lt;span class="c"&gt;# @see https://stackoverflow.com/a/65348102/413531 &lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;ssh-keygen &lt;span class="nt"&gt;-A&lt;/span&gt;

&lt;span class="c"&gt;# we use SSH deployment configuration in PhpStorm for local development&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 22&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/usr/sbin/sshd", "-D"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="setup-phpstorm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup PhpStorm
&lt;/h2&gt;

&lt;p&gt;We will configure a remote PHP interpreter that uses an SSH connection to run commands in the  &lt;code&gt;application&lt;/code&gt; container. Before,  &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/#configure-the-deployment-configuration" rel="noopener noreferrer"&gt;we have been using an &lt;code&gt;SFTP Deployment configuration&lt;/code&gt;&lt;/a&gt; , which was kinda confusing ("What is SFTP doing here?"), so we will use an  &lt;a href="https://www.jetbrains.com/help/phpstorm/create-ssh-configurations.html" rel="noopener noreferrer"&gt;SSH Configuration&lt;/a&gt;  instead and configure the path mappings in the &lt;strong&gt;Cli Interpreter&lt;/strong&gt; interface&lt;/p&gt;

&lt;p&gt;&lt;a id="ssh-configuration"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH Configuration
&lt;/h3&gt;

&lt;p&gt;At &lt;code&gt;File | Settings | Tools | SSH Configurations&lt;/code&gt; create a new SSH Configuration named  "Docker PHP Tutorial" with the following settings &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host: 127.0.0.1&lt;/li&gt;
&lt;li&gt;Port: see &lt;code&gt;APPLICATION_SSH_HOST_PORT&lt;/code&gt; in &lt;code&gt;.docker/docker-compose/docker-compose.local.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;User name: see &lt;code&gt;APP_USER_NAME&lt;/code&gt; in &lt;code&gt;.make/.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Authentication type: Password&lt;/li&gt;
&lt;li&gt;Password: see &lt;code&gt;APP_SSH_PASSWORD&lt;/code&gt; in &lt;code&gt;.docker/.env&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-ssh-configuration.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-ssh-configuration.PNG" title="PhpStorm SSH Configuration" alt="PhpStorm SSH Configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="php-interpreter"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Interpreter
&lt;/h3&gt;

&lt;p&gt;At &lt;code&gt;File | Settings | PHP&lt;/code&gt; add a new PHP CLI interpreter that uses the new SSH Configuration&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-cli-interpreter.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-cli-interpreter.PNG" title="PhpStorm new CLI interpreter" alt="PhpStorm new CLI interpreter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition, we define the &lt;strong&gt;path to the xdebug extension&lt;/strong&gt; because it is disabled by default but PhpStorm can enable it automatically if required. You can find the path in the &lt;code&gt;application&lt;/code&gt; container via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# php -i | grep extension_dir
extension_dir =&amp;gt; /usr/lib/php8/modules =&amp;gt; /usr/lib/php8/modules
root:/var/www/app# ll /usr/lib/php8/modules | grep xdebug
-rwxr-xr-x    1 root     root        303936 Jan  9 00:21 xdebug.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We still need to  &lt;a href="https://www.pascallandau.com/blog/setup-phpstorm-with-xdebug-on-docker/#fix-xdebug-on-phpstorm-when-run-from-a-docker-container" rel="noopener noreferrer"&gt;Fix Xdebug on PhpStorm when run from a Docker container&lt;/a&gt; by adding a custom PHP option for &lt;code&gt;xdebug.client_host=host.docker.internal&lt;/code&gt;. That's the same value we use in &lt;code&gt;.docker/images/php/base/conf.d/zz-app-local.ini&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-cli-interpreter-xdebug.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-cli-interpreter-xdebug.PNG" title="PhpStorm Xdebug settings for the CLI interpreter" alt="PhpStorm Xdebug settings for the CLI interpreter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the interpreter overview we must now configure the &lt;strong&gt;path mappings&lt;/strong&gt; so that PhpStorm knows "which local file belongs to which remote one". The remote folder is defined in &lt;code&gt;.docker/.env&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_CODE_PATH_CONTAINER=/var/www/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-cli-interpreter-path-mappings.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-cli-interpreter-path-mappings.PNG" title="PhpStorm path mappings for the CLI interpreter" alt="PhpStorm path mappings for the CLI interpreter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Afterwards we can set a breakpoint e.g. in &lt;code&gt;setup.php&lt;/code&gt; and start debugging:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-xdebug-breakpoint.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-xdebug-breakpoint.PNG" title="PhpStorm debugging breakpoint" alt="PhpStorm debugging breakpoint"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot shows that PhpStorm adds the Xdebug extension that we defined previously.&lt;/p&gt;

&lt;p&gt;&lt;a id="phpunit"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  PHPUnit
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;phpunit&lt;/code&gt; is configured via &lt;code&gt;File | Settings | PHP | Test Frameworks&lt;/code&gt;. First, we select the interpreter that we just added&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-phpunit-interpreter.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-phpunit-interpreter.PNG" title="Set up phpunit in PhpStorm" alt="Set up phpunit in PhpStorm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, we add the paths to the composer autoload script and the &lt;code&gt;phpunit.xml&lt;/code&gt; configuration file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-phpunit-settings.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-phpunit-settings.PNG" title="phpunit settings in PhpStorm" alt="phpunit settings in PhpStorm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PhpStorm will now execute tests using the PHP interpreter in the &lt;code&gt;application&lt;/code&gt; container&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-run-phpunit-test.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-run-phpunit-test.PNG" title="Run a phpunit test in PhpStorm" alt="Run a phpunit test in PhpStorm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="debugging"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging
&lt;/h3&gt;

&lt;p&gt;First of all, if you haven't already please also take a look at the  &lt;a href="https://xdebug.org/docs/step_debug" rel="noopener noreferrer"&gt;official xdebug documentation&lt;/a&gt;. Derick is doing a great job  at explaining xdebug in detail including some helpful videos like &lt;a href="https://www.youtube.com/watch?v=4opFac50Vwo" rel="noopener noreferrer"&gt;Xdebug 3: Xdebug with Docker and PhpStorm in 5 minutes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="debug-code-executed-via-phpstorm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Debug code executed via PhpStorm
&lt;/h4&gt;

&lt;p&gt;This should already work out of the box. Simply set a break point, right-click on a file and choose  "Debug '...'"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-xdebug-breakpoint.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-xdebug-breakpoint.PNG" title="PhpStorm debugging breakpoint" alt="PhpStorm debugging breakpoint"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="debug-code-executed-via-php-fpm-cli-or-from-a-worker"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Debug code executed via php-fpm, cli or from a worker
&lt;/h4&gt;

&lt;p&gt;For code that is executed "directly" by a container without PhpStorm, we first need to enable  &lt;code&gt;xdebug&lt;/code&gt; in the container by removing the &lt;code&gt;;&lt;/code&gt; in front of the extension in  &lt;code&gt;/etc/php8/conf.d/zz-app-local.ini&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; Note:
; Remove the comment ; to enable debugging
zend_extension=xdebug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this a little more convenient, we use dedicated make recipes for those actions in &lt;code&gt;.make/01-01-application-commands.mk&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;execute-in-container&lt;/span&gt;
&lt;span class="nl"&gt;execute-in-container&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Execute a command in a container. E.g. via "make execute-in-container DOCKER_SERVICE_NAME=php-fpm COMMAND="echo 'hello'"&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error DOCKER_SERVICE_NAME is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;COMMAND&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error COMMAND is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;COMMAND&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;enable-xdebug&lt;/span&gt;
&lt;span class="nl"&gt;enable-xdebug&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Enable xdebug in the given container specified by "DOCKER_SERVICE_NAME". E.g. "make enable-xdebug DOCKER_SERVICE_NAME=php-fpm"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; execute-in-container &lt;span class="nv"&gt;APP_USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="nv"&gt;DOCKER_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sed -i 's/.*zend_extension=xdebug/zend_extension=xdebug/' '/etc/php8/conf.d/zz-app-local.ini'"&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;disable-xdebug&lt;/span&gt;
&lt;span class="nl"&gt;disable-xdebug&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Disable xdebug in the given container specified by "DOCKER_SERVICE_NAME". E.g. "make enable-xdebug DOCKER_SERVICE_NAME=php-fpm"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; execute-in-container &lt;span class="nv"&gt;APP_USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="nv"&gt;DOCKER_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sed -i 's/.*zend_extension=xdebug/;zend_extension=xdebug/' '/etc/php8/conf.d/zz-app-local.ini'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To capture incoming requests, we need to make PhpStorm listen for PHP Debug connections via &lt;code&gt;Run | Start Listening for PHP Debug Connections&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-start-listening-for-debug-connections.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-start-listening-for-debug-connections.PNG" title="PhpStorm: Start Listening for PHP Debug Connections" alt="PhpStorm: Start Listening for PHP Debug Connections"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The corresponding ports are configured at &lt;code&gt;File | Settings | PHP | Debug&lt;/code&gt;. In Xdebug &amp;lt; 3 the  default port was &lt;code&gt;9000&lt;/code&gt; and in &lt;a href="https://xdebug.org/docs/all_settings#client_port" rel="noopener noreferrer"&gt;Xdebug 3 it is &lt;code&gt;9003&lt;/code&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-xdebug-ports.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-xdebug-ports.PNG" title="PhpStorm: configure xdebug ports" alt="PhpStorm: configure xdebug ports"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we need to add a server via &lt;code&gt;File | Settings | PHP | Servers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-server.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-server.PNG" title="PhpStorm: configure a server" alt="PhpStorm: configure a server"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The name of the server must match the value of the &lt;code&gt;serverName&lt;/code&gt; key in the environment variable  &lt;code&gt;PHP_IDE_CONFIG&lt;/code&gt; that we configured previously as &lt;code&gt;serverName=dofroscra&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="php-fpm"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  php-fpm
&lt;/h5&gt;

&lt;p&gt;For &lt;code&gt;php-fpm&lt;/code&gt; we must &lt;a href="https://stackoverflow.com/a/43076457" rel="noopener noreferrer"&gt;restart the &lt;code&gt;php-fpm&lt;/code&gt; process without restarting the container&lt;/a&gt; after we have activated &lt;code&gt;xdebug&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kill -USR2 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this is a pain to remember, we add a make target in &lt;code&gt;.make/01-01-application-commands.mk&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# @see https://stackoverflow.com/a/43076457
&lt;/span&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;restart-php-fpm&lt;/span&gt;
&lt;span class="nl"&gt;restart-php-fpm&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Restart the php-fpm service&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;MAKE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; execute-in-container &lt;span class="nv"&gt;DOCKER_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME_PHP_FPM&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"kill -USR2 1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we can now simply run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make enable-xdebug DOCKER_SERVICE_NAME=php-fpm
make restart-php-fpm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting a breakpoint in &lt;code&gt;public/index.php&lt;/code&gt; and opening &lt;a href="http://127.0.0.1/" rel="noopener noreferrer"&gt;http://127.0.0.1/&lt;/a&gt; in a browser or via &lt;code&gt;curl http://127.0.0.1/&lt;/code&gt; will halt the execution as expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-xdebug-breakpoint-php-fpm.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-xdebug-breakpoint-php-fpm.PNG" title="PhpStorm debugging breakpoint for php-fpm" alt="PhpStorm debugging breakpoint for php-fpm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="cli"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  cli
&lt;/h5&gt;

&lt;p&gt;Instead of triggering a PHP script via HTTP request, we can also run CLI scripts - think of the  &lt;code&gt;make setup-db&lt;/code&gt; target for instance. To debug such invocations, we need to follow the same steps as before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enable the &lt;code&gt;xdebug&lt;/code&gt; extension in the &lt;code&gt;application&lt;/code&gt; container&lt;/li&gt;
&lt;li&gt;"Listening for PHP Debug Connections" from PhpStorm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running the following make targets will trigger a breakpoint in &lt;code&gt;setup.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make enable-xdebug DOCKER_SERVICE_NAME=application
make setup-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-xdebug-breakpoint-cli.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-xdebug-breakpoint-cli.PNG" title="PhpStorm debugging breakpoint for cli" alt="PhpStorm debugging breakpoint for cli"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="php-workers"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  php-workers
&lt;/h5&gt;

&lt;p&gt;And finally the same thing for long running PHP processes (aka workers). Just as before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enable the &lt;code&gt;xdebug&lt;/code&gt; extension in the &lt;code&gt;php-worker&lt;/code&gt; container&lt;/li&gt;
&lt;li&gt;"Listening for PHP Debug Connections" from PhpStorm&lt;/li&gt;
&lt;li&gt;restart the php workers
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running the following make targets will trigger a breakpoint in &lt;code&gt;worker.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make enable-xdebug DOCKER_SERVICE_NAME=php-worker
make restart-workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/phpstorm-docker-xdebug-3-php-8-1-in-2022/phpstorm-xdebug-breakpoint-php-worker.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fphpstorm-docker-xdebug-3-php-8-1-in-2022%2Fphpstorm-xdebug-breakpoint-php-worker.PNG" title="PhpStorm debugging breakpoint for php-workers" alt="PhpStorm debugging breakpoint for php-workers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="strace"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  strace
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://strace.io/" rel="noopener noreferrer"&gt;strace&lt;/a&gt; is a great tool for debugging long running processes that I've  adopted after reading &lt;a href="https://derickrethans.nl/what-is-php-doing.html" rel="noopener noreferrer"&gt;What is PHP doing?&lt;/a&gt;. I've added it to the php base image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        strace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can attach to any running process via &lt;code&gt;sudo strace -p $processId&lt;/code&gt; - BUT that doesn't work  out of the box on docker and will fail with the error message&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strace: attach: ptrace(PTRACE_SEIZE, 1): Operation not permitted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is caused by a security measure from docker and  &lt;a href="https://stackoverflow.com/a/46676868" rel="noopener noreferrer"&gt;can be circumvented&lt;/a&gt; by adding&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SYS_PTRACE"&lt;/span&gt;
    &lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seccomp=unconfined"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in &lt;code&gt;.docker/docker-compose/docker-compose.local.yml&lt;/code&gt; &lt;strong&gt;to all PHP containers&lt;/strong&gt;. After  rebuilding and restarting the docker setup, you can now e.g. log in the &lt;code&gt;php-worker&lt;/code&gt; container and run &lt;code&gt;strace&lt;/code&gt; on a php worker process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;application:/var/www/app# ps aux
PID   USER     TIME  COMMAND
    1 applicat  0:00 {supervisord} /usr/bin/python3 /usr/bin/supervisord
    7 applicat  0:00 php /var/www/app/worker.php
    8 applicat  0:00 php /var/www/app/worker.php
    9 applicat  0:00 php /var/www/app/worker.php
   10 applicat  0:00 php /var/www/app/worker.php
   11 applicat  0:00 bash
   20 applicat  0:00 ps aux
application:/var/www/app# sudo strace -p 7
strace: Process 7 attached
restart_syscall(&amp;lt;... resuming interrupted read ...&amp;gt;) = 0
poll([{fd=4, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 0 (Timeout)
sendto(4, "*2\r\n$4\r\nRPOP\r\n$5\r\nqueue\r\n", 25, MSG_DONTWAIT, NULL, 0) = 25
poll([{fd=4, events=POLLIN|POLLPRI|POLLERR|POLLHUP}], 1, 0) = 1 ([{fd=4, revents=POLLIN}])
recvfrom(4, "$", 1, MSG_PEEK, NULL, NULL) = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. Apart from that, you should now have a fully configured development setup that works with PhpStorm as your IDE.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/" rel="noopener noreferrer"&gt;use a fresh installation of Laravel on top of our setup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>php</category>
      <category>phpstorm</category>
      <category>xdebug</category>
      <category>docker</category>
    </item>
    <item>
      <title>Docker from scratch for PHP 8.1 Applications in 2022 [Tutorial Part 2]</title>
      <dc:creator>Pascal Landau</dc:creator>
      <pubDate>Mon, 20 Jun 2022 07:40:19 +0000</pubDate>
      <link>https://dev.to/pascallandau/docker-from-scratch-for-php-81-applications-in-2022-tutorial-part-41-h01</link>
      <guid>https://dev.to/pascallandau/docker-from-scratch-for-php-81-applications-in-2022-tutorial-part-41-h01</guid>
      <description>&lt;p&gt;This article appeared first on &lt;a href="https://www.pascallandau.com/" rel="noopener noreferrer"&gt;https://www.pascallandau.com/&lt;/a&gt; at &lt;a href="https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/" rel="noopener noreferrer"&gt;Docker from scratch for PHP 8.1 Applications in 2022 [Tutorial Part 2]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In this part of the tutorial series on developing PHP on Docker we will revisit the previous tutorials and update some things to be up-to-date in 2022.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/NuSWKx9FSso"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All code samples are publicly available&lt;/strong&gt; in my &lt;a href="https://github.com/paslandau/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial repository on Github&lt;/a&gt;.   You find the branch for this tutorial at  &lt;a href="https://github.com/paslandau/docker-php-tutorial/tree/part-4-1-docker-from-scratch-for-php-applications-in-2022" rel="noopener noreferrer"&gt;part-4-1-docker-from-scratch-for-php-applications-in-2022&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All published parts of the Docker PHP Tutorial&lt;/strong&gt; are collected under a dedicated page at &lt;a href="https://www.pascallandau.com/docker-php-tutorial/" rel="noopener noreferrer"&gt;Docker PHP Tutorial&lt;/a&gt;. The previous part was &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/" rel="noopener noreferrer"&gt;Structuring the Docker setup for PHP Projects&lt;/a&gt; and the following one is &lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/" rel="noopener noreferrer"&gt;PhpStorm, Docker and Xdebug 3 on PHP 8.1 in 2022&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along, please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get &lt;strong&gt;automatic notifications&lt;/strong&gt; when the next part comes out :)&lt;/p&gt;

&lt;p&gt;&lt;a id="table-of-contents"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Local docker setup&lt;/li&gt;
&lt;li&gt;
Docker

&lt;ul&gt;
&lt;li&gt;
docker-compose

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.docker/.env&lt;/code&gt; file and required ENV variables&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Images

&lt;ul&gt;
&lt;li&gt;
PHP images

&lt;ul&gt;
&lt;li&gt;ENV vs ARG&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Image naming convention&lt;/li&gt;

&lt;li&gt;Environments and build targets&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

Makefile

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.make/*.mk&lt;/code&gt; includes&lt;/li&gt;
&lt;li&gt;
Shared variables: &lt;code&gt;.make/.env&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Manual modifications&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Enforce required parameters&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

Make + Docker = &amp;lt;3

&lt;ul&gt;
&lt;li&gt;Ensuring the build order&lt;/li&gt;
&lt;li&gt;
Run commands in the docker containers

&lt;ul&gt;
&lt;li&gt;Solving permission issues&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;PHP POC&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="introduction"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you have read the previous tutorial &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/" rel="noopener noreferrer"&gt;Structuring the Docker setup for PHP Projects&lt;/a&gt; you might encounter some significant changes. The tutorial was published over 2 years ago,  Docker has evolved and I have learned more about it. Plus, I gathered practical  experience (good and bad) with the previous setup. I would now consider most of the points under &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#fundamentals-on-building-the-containers" rel="noopener noreferrer"&gt;Fundamentals on building the containers&lt;/a&gt; as either "not required" or simply "overengineered / too complex". To be concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#setting-the-timezone" rel="noopener noreferrer"&gt;Setting the timezone&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;not required if the default is already UTC (which is almost always the case)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#synchronizing-file-and-folder-ownership-on-shared-volumes" rel="noopener noreferrer"&gt;Synchronizing file and folder ownership on shared volumes&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;this is only an issue if files need to be &lt;strong&gt;modified&lt;/strong&gt; by containers and the host system - which
is only really relevant for the PHP containers&lt;/li&gt;
&lt;li&gt;in addition, I would recommend adding a completely new user (e.g. &lt;code&gt;application&lt;/code&gt;) instead of
re-using an existing one like &lt;code&gt;www-data&lt;/code&gt; - this simplifies the whole user setup &lt;em&gt;a lot&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;from now on we will be using &lt;code&gt;application&lt;/code&gt; as the user name (&lt;code&gt;APP_USER_NAME&lt;/code&gt;) and &lt;code&gt;10000&lt;/code&gt; the 
user id (&lt;code&gt;APP_USER_ID&lt;/code&gt;; following the best practice to 
&lt;a href="https://github.com/hexops/dockerfile#do-not-use-a-uid-below-10000" rel="noopener noreferrer"&gt;not use a UID below 10,000&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#modifying-configuration-files" rel="noopener noreferrer"&gt;Modifying configuration files&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;just use &lt;code&gt;sed&lt;/code&gt; - no need for a dedicated script&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#installing-php-extensions" rel="noopener noreferrer"&gt;Installing php extensions&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;see PHP images - will now be done via &lt;code&gt;apk add&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#installing-common-software" rel="noopener noreferrer"&gt;Installing common software&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;see PHP images - since there is only one base image there is no need for a
dedicated script&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#cleaning-up" rel="noopener noreferrer"&gt;Cleaning up&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;didn't really make sense because the "cleaned up files" were already part of a previous layer&lt;/li&gt;
&lt;li&gt;we might "bring it back" later when we optimize the image size to speed up the pushing/pulling
of the images to/from the registry&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#providing-host-docker-internal-for-linux-host-systems" rel="noopener noreferrer"&gt;Providing host.docker.internal for linux host systems&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can now be done via
the &lt;a href="https://github.com/docker/for-linux/issues/264#issuecomment-965465879" rel="noopener noreferrer"&gt;&lt;code&gt;host-gateway&lt;/code&gt; magic reference&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;myservice&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;host.docker.internal:host-gateway&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;thus, no custom entrypoint is required any longer&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="local-docker-setup"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Local docker setup
&lt;/h2&gt;

&lt;p&gt;The goal of this part is the introduction of a working local setup &lt;strong&gt;without development tools&lt;/strong&gt;. In other words: We want the bare minimum to have something running locally.&lt;/p&gt;

&lt;p&gt;The main components are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;make&lt;/code&gt; setup in the &lt;code&gt;Makefile&lt;/code&gt; and in the &lt;code&gt;.make/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;the docker setup in the &lt;code&gt;.docker/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;some PHP files that act as a POC for the end2end functionality of the docker setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the code via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git checkout part_4_section_1_docker_from_scratch_for_php_applications_in_2022_code_structure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;initialize it via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make make-init
make docker-build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and run it via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make docker-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can access the web interface via &lt;a href="http://127.0.0.1" rel="noopener noreferrer"&gt;http://127.0.0.1&lt;/a&gt;. The following  diagram shows how the containers are connected&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/docker-from-scratch-for-php-applications-in-2022/docker-containers.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fdocker-from-scratch-for-php-applications-in-2022%2Fdocker-containers.PNG" title="Docker container connections" alt="Docker container connections"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See also the PHP POC for a full test of the setup.&lt;/p&gt;

&lt;p&gt;&lt;a id="docker"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker
&lt;/h2&gt;

&lt;p&gt;The docker setup consists of&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an nginx container as a webserver&lt;/li&gt;
&lt;li&gt;a MySQL database container&lt;/li&gt;
&lt;li&gt;a Redis container that acts as a queue&lt;/li&gt;
&lt;li&gt;a php base image that is used by

&lt;ul&gt;
&lt;li&gt;a php worker container that spawns multiple PHP worker processes via &lt;code&gt;supervisor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a php-fpm container as a backend for the nginx container&lt;/li&gt;
&lt;li&gt;an application container that we use to run commands&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/docker-from-scratch-for-php-applications-in-2022/docker-images.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fdocker-from-scratch-for-php-applications-in-2022%2Fdocker-images.PNG" title="Docker images" alt="Docker images"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We keep the &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#the-docker-folder" rel="noopener noreferrer"&gt;&lt;code&gt;.docker/&lt;/code&gt; directory from the previous tutorial&lt;/a&gt; , though it will be split into &lt;code&gt;docker-compose/&lt;/code&gt; and &lt;code&gt;images/&lt;/code&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
└── .docker/
    ├── docker-compose/
    |   ├── docker-compose.yml
    |   └── &amp;lt;other docker-compose files&amp;gt;
    ├── images/
    |   ├── nginx/
    |   |   ├── Dockerfile
    |   |   └── &amp;lt;other files for the nginx image&amp;gt;
    |   └── &amp;lt;other folders for docker images&amp;gt;
    ├── .env
    └── .env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="docker-compose"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  docker-compose
&lt;/h3&gt;

&lt;p&gt;All images are &lt;strong&gt;build&lt;/strong&gt; via &lt;code&gt;docker-compose&lt;/code&gt; because the &lt;code&gt;docker-compose.yml&lt;/code&gt; file(s) provide a nice abstraction layer for the build configuration. In addition, we can also use it to &lt;strong&gt;orchestrate&lt;/strong&gt; the containers, i.e. control volumes, port mappings, networking, etc. - as well as start and stop them via &lt;code&gt;docker-compose up&lt;/code&gt; and &lt;code&gt;docker-compose down&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;FYI: Even though it is &lt;em&gt;convenient&lt;/em&gt; to use &lt;code&gt;docker-compose&lt;/code&gt; for both things, I found it also to make the setup more complex than it needs to be when running things later in production (when we are &lt;em&gt;not&lt;/em&gt; using &lt;code&gt;docker-compose&lt;/code&gt; any longer). I believe the problem here is that some modifications are ONLY required for building while others are ONLY required for running - and combining both in the same file yields a certain amount of noise. But: It is what it is.&lt;/p&gt;

&lt;p&gt;We use three separate &lt;code&gt;docker-compose.yml&lt;/code&gt; files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;docker-compose.yml

&lt;ul&gt;
&lt;li&gt;contains all information valid for all environments&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;docker-compose.local.yml

&lt;ul&gt;
&lt;li&gt;contains information specific to the &lt;code&gt;local&lt;/code&gt; environment,
see Environments and build targets
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;docker-compose-php-base.yml

&lt;ul&gt;
&lt;li&gt;contains information for building the php base image, see PHP images
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a id="docker-env-file-and-required-env-variables"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;.docker/.env&lt;/code&gt; file and required ENV variables
&lt;/h4&gt;

&lt;p&gt;In our docker setup we basically have 3 different types of variables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;variables that &lt;strong&gt;depend on the local setup&lt;/strong&gt; of an individual developer, e.g. the
&lt;code&gt;NGINX_HOST_HTTP_PORT&lt;/code&gt; on the host machine (because the default one might already be in use) 2. variables that &lt;strong&gt;are used in multiple images&lt;/strong&gt;, e.g. the location of the codebase within a
container's file system 3. variables that &lt;strong&gt;hold information that is "likely to change"&lt;/strong&gt;, e.g. the exact version of a base
image&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since - again - we strive to retain a single source of truth, we extract the information as variables and put them in a &lt;code&gt;.docker/.env&lt;/code&gt; file. In a perfect world, I would like to separate these different types in different files - but &lt;code&gt;docker-compose&lt;/code&gt; only allows a single &lt;code&gt;.env&lt;/code&gt; file, see e.g. &lt;a href="https://github.com/docker/compose/issues/6170#issuecomment-443523663" rel="noopener noreferrer"&gt;this comment&lt;/a&gt;. If the file does not exist, it is copied from &lt;code&gt;.docker/.env.example&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The variables are then used in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file(s). I found it to be "the least  painful" to always use the &lt;a href="https://docs.docker.com/compose/environment-variables/#substitute-environment-variables-in-compose-files" rel="noopener noreferrer"&gt;&lt;code&gt;?&lt;/code&gt; modifier on variables&lt;/a&gt; so that &lt;code&gt;docker-compose&lt;/code&gt; &lt;strong&gt;fails immediately if the variable is missing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Note: Some variables are expected to be passed via environment variables when &lt;code&gt;docker-compose&lt;/code&gt; is invoked (i.e. they are required but not defined in the &lt;code&gt;.env&lt;/code&gt; file; see also  Shared variables: &lt;code&gt;.make/.env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="images"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Images
&lt;/h3&gt;

&lt;p&gt;For &lt;strong&gt;MySQL&lt;/strong&gt; and &lt;strong&gt;redis&lt;/strong&gt; we do not use custom-built images but instead &lt;strong&gt;use the official ones &lt;em&gt;directly&lt;/em&gt;&lt;/strong&gt; and configure them through environment variables when starting the containers. In production, we won't use docker anyway for these services but instead rely on the managed versions, e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redis =&amp;gt; &lt;a href="https://cloud.google.com/memorystore/docs/redis" rel="noopener noreferrer"&gt;Memorystore for Redis (GCP)&lt;/a&gt; or
&lt;a href="https://aws.amazon.com/de/elasticache/redis/" rel="noopener noreferrer"&gt;ElastiCache für Redis (AWS)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;mysql =&amp;gt; &lt;a href="https://cloud.google.com/sql/docs/mysql" rel="noopener noreferrer"&gt;Cloud SQL for MySQL (GCP)&lt;/a&gt; or
&lt;a href="https://aws.amazon.com/de/rds/mysql/" rel="noopener noreferrer"&gt;RDS for MySQL (AWS)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The remaining containers are defined in their respective subdirectories in the &lt;code&gt;.docker/images/&lt;/code&gt; directory, e.g. the image for the &lt;code&gt;nginx&lt;/code&gt; container is build via the &lt;code&gt;Dockerfile&lt;/code&gt; located in &lt;code&gt;.docker/images/nginx/Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="php-images"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  PHP images
&lt;/h4&gt;

&lt;p&gt;We need 3 different PHP images (fpm, workers, application) and use a slightly different approach than in &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/" rel="noopener noreferrer"&gt;Structuring the Docker setup for PHP Projects&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;Instead of using the &lt;a href="https://hub.docker.com/_/php" rel="noopener noreferrer"&gt;official PHP base images&lt;/a&gt; (i.e. cli or fpm), we use a "plain" alpine base image and install PHP and the required extensions manually in it. This allows us to build a common base image for all PHP images. Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a central place for shared tools and configuration (no more need for a &lt;code&gt;.shared/&lt;/code&gt; directory)&lt;/li&gt;
&lt;li&gt;reduced image size when pushing the individual images (the base image is recognized as a layer and
thus "already exists")&lt;/li&gt;
&lt;li&gt;installing extensions via &lt;code&gt;apk add&lt;/code&gt; is &lt;strong&gt;a lot&lt;/strong&gt; faster than via &lt;code&gt;docker-php-ext-install&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This new approach has two major downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we depend on the alpine release cycle of PHP (and PHP extensions)&lt;/li&gt;
&lt;li&gt;the image build process is more complex, because we must build the base image first before we can
build the final images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, both issues can be solved rather easily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/codecasts/php-alpine" rel="noopener noreferrer"&gt;codecasts/php-alpine&lt;/a&gt; maintains an &lt;code&gt;apk&lt;/code&gt; repository with
the latest PHP versions for alpine&lt;/li&gt;
&lt;li&gt;we use a dedicated &lt;code&gt;make&lt;/code&gt; target to build the images instead of invoking &lt;code&gt;docker-compose&lt;/code&gt;
directly - this enables us to define a "build order" (base first, rest after) while still having
to run only a single command as a developer 
(see Ensuring the build order)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="env-vs-arg"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  ENV vs ARG
&lt;/h5&gt;

&lt;p&gt;I've noticed that some build arguments are required in multiple PHP containers, e.g. the name of the application user defined in the &lt;code&gt;APP_USER_NAME&lt;/code&gt; ENV variable. The username is needed&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in the base image to create the user&lt;/li&gt;
&lt;li&gt;in the fpm image to define the user that runs the fpm processes (see &lt;code&gt;php-fpm.d/www.conf&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;in the worker image to define the user that runs the worker processes (
see &lt;code&gt;supervisor/supervisord.conf&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of passing the name to all images via build argument, i.e.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define it explicitly under &lt;code&gt;services.*.build.args&lt;/code&gt; in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;"retrieve" it in the Dockerfile via &lt;code&gt;ARG APP_USER_NAME&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've opted to make the username available as an &lt;code&gt;ENV&lt;/code&gt; variable in the base image via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG APP_USER_NAME
ENV APP_USER_NAME=${APP_USER_NAME}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and thus be able to access it in the child images directly, I can now write&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN echo ${APP_USER_NAME}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG APP_USER_NAME
RUN echo ${APP_USER_NAME}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not 100% certain that I like this approach as I'm more or less "abusing" ENV variables in ways that they are likely not intended ("Why would the username need to be stored as an ENV variable?") -&lt;br&gt;
but I also don't see any other practical downside yet.&lt;/p&gt;



&lt;p&gt;&lt;a id="image-naming-convention"&gt; &lt;/a&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  Image naming convention
&lt;/h4&gt;

&lt;p&gt;Defining a &lt;a href="https://windsock.io/referencing-docker-images/" rel="noopener noreferrer"&gt;fully qualified name for images&lt;/a&gt; will make it much easier to reference the images later, e.g. when pushing them to the registry.&lt;/p&gt;

&lt;p&gt;The naming convention for the images is &lt;code&gt;$(DOCKER_REGISTRY)/$(DOCKER_NAMESPACE)/$(DOCKER_SERVICE_NAME)-$(ENV)&lt;/code&gt;, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   docker.io/dofroscra/nginx-local
$(DOCKER_REGISTRY)---^          ^        ^     ^        docker.io
$(DOCKER_NAMESPACE)-------------^        ^     ^        dofroscra
$(DOCKER_SERVICE_NAME)-------------------^     ^        nginx
$(ENV)-----------------------------------------^        local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it is used as value for &lt;code&gt;services.*.image&lt;/code&gt;, e.g. for &lt;code&gt;nginx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/nginx-${ENV?}:${TAG?}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case you are wondering: &lt;code&gt;dofroscra&lt;/code&gt; stems from &lt;strong&gt;Do&lt;/strong&gt;cker &lt;strong&gt;Fro&lt;/strong&gt;m &lt;strong&gt;Scra&lt;/strong&gt;tch&lt;/p&gt;

&lt;p&gt;&lt;a id="environments-and-build-targets"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Environments and build targets
&lt;/h4&gt;

&lt;p&gt;Our final goal is a setup that we can use for&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local development&lt;/li&gt;
&lt;li&gt;in a CI/CD pipeline&lt;/li&gt;
&lt;li&gt;in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and even though we strive to for a &lt;a href="https://12factor.net/dev-prod-parity" rel="noopener noreferrer"&gt;parity between those different environments&lt;/a&gt;, there will be differences due to fundamentally different requirements. E.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on &lt;em&gt;production&lt;/em&gt; I want a container &lt;strong&gt;including the sourcecode without any test dependencies&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;on &lt;em&gt;CI&lt;/em&gt; I want a container &lt;strong&gt;including the sourcecode WITH test dependencies&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;on &lt;em&gt;local&lt;/em&gt; I want a container &lt;strong&gt;that mounts the sourcecode from my host (including 
dependencies)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is reflected through the &lt;code&gt;ENV&lt;/code&gt; environment variable. We use it in two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;as part of the image name as a suffix of the service name
(see Image naming convention) 2. to specify
the &lt;a href="https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target" rel="noopener noreferrer"&gt;target build stage&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;See the &lt;code&gt;docker-compose-php-base.yml&lt;/code&gt; file for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  php-base:
    image: ${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-base-${ENV?}:${TAG?}
    build:
      dockerfile: images/php/base/Dockerfile
      target: ${ENV?}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;strong&gt;multiple targets in the same Dockerfile&lt;/strong&gt; enables us to keep a &lt;strong&gt;common base&lt;/strong&gt; but also  include &lt;strong&gt;environment specific instructions&lt;/strong&gt;. See the Dockerfile of the &lt;code&gt;php-base&lt;/code&gt; image for  example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; ALPINE_VERSION&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;composer:${COMPOSER_VERSION}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;composer&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;alpine:${ALPINE_VERSION}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        bash

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; $APP_CODE_PATH&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        mysql-client &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;it first defines a &lt;code&gt;base&lt;/code&gt; stage that includes software required in all environments&lt;/li&gt;
&lt;li&gt;and then defines a &lt;code&gt;local&lt;/code&gt; stage that adds additionally a &lt;code&gt;mysql-client&lt;/code&gt; that helps us to debug
connectivity issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the build for &lt;code&gt;local&lt;/code&gt; is finished, we end up with an image called &lt;code&gt;php-base-local&lt;/code&gt; that used the &lt;code&gt;local&lt;/code&gt; build stage as target build stage.&lt;/p&gt;

&lt;p&gt;&lt;a id="makefile"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Makefile
&lt;/h2&gt;

&lt;p&gt;In the following section I will &lt;strong&gt;introduce a couple of commands&lt;/strong&gt;, e.g. for building and running  containers. And to be honest, I find it kinda challenging to keep them in mind without having to  look up the exact options and arguments. I would &lt;strong&gt;usually create a helper function&lt;/strong&gt; or an alias in my  local &lt;code&gt;.bashrc&lt;/code&gt; file in a situation like that - but that wouldn't be available to other members of  the team then and it would be very specific to this one project.&lt;/p&gt;

&lt;p&gt;Instead we'll use a &lt;strong&gt;self-documenting Makefile&lt;/strong&gt; that &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#using-make-as-central-entry-point" rel="noopener noreferrer"&gt;acts as the central entrypoint in the application&lt;/a&gt;.  Since Makefiles tend to grow over time, I've adopted some strategies to keep them  "sane" via includes, shared variables and better error handling.&lt;/p&gt;

&lt;p&gt;&lt;a id="make-mk-includes"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.make/*.mk&lt;/code&gt; includes
&lt;/h3&gt;

&lt;p&gt;Over time the &lt;code&gt;make&lt;/code&gt; setup will grow substantially, thus we split it into multiple &lt;code&gt;.mk&lt;/code&gt; files in the &lt;code&gt;.make/&lt;/code&gt; directory. The individual files are prefixed with a number to ensure their order when we include them in the main &lt;code&gt;Makefile&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include .make/*.mk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
└── .make/
    ├── 01-00-application-setup.mk
    ├── 01-01-application-commands.mk
    └── 02-00-docker.mk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="shared-variables-make-env"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared variables: &lt;code&gt;.make/.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We try to make &lt;strong&gt;shared variables&lt;/strong&gt; available here, because we can then pass them on to individual commands as a prefix, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;some-target&lt;/span&gt;
&lt;span class="nl"&gt;some-target&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Run some target&lt;/span&gt;
    &lt;span class="nv"&gt;ENV_FOO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;BAR some_command &lt;span class="nt"&gt;--baz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will make the &lt;code&gt;ENV_FOO&lt;/code&gt; available as environment variable to &lt;code&gt;some_command&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Shared variables are used by different components, and we always try to maintain only a &lt;strong&gt;single source of truth&lt;/strong&gt;. An example would be the &lt;code&gt;DOCKER_REGISTRY&lt;/code&gt; variable that we need to define the image names of our docker images in the &lt;code&gt;docker-compose.yml&lt;/code&gt; files but also when pushing/pulling/deploying images via make targets later. In this case, the variable is required by &lt;code&gt;make&lt;/code&gt; as well as &lt;code&gt;docker-compose&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To have a clear separation between variables and "code", we use a &lt;code&gt;.env&lt;/code&gt; file located at &lt;code&gt;. make/.env&lt;/code&gt;. It can be initialized via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make make-init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;by copying the &lt;code&gt;.make/.env.example&lt;/code&gt; to &lt;code&gt;.make/.env&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
└── .make/
    ├── .make/.env.example
    └── .make/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is included in the main &lt;code&gt;Makefile&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-include .make/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-&lt;/code&gt; prefix ensures that make doesn't fail if the file does not exist (yet), see &lt;a href="https://www.gnu.org/software/make/manual/html_node/Include.html" rel="noopener noreferrer"&gt;GNU make: Including Other Makefiles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="manual-modifications"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Manual modifications
&lt;/h4&gt;

&lt;p&gt;You can always &lt;strong&gt;modify the &lt;code&gt;.make/.env&lt;/code&gt; file manually if required&lt;/strong&gt;. This might be the  case when you run &lt;code&gt;docker&lt;/code&gt; on Linux and need to match the &lt;code&gt;user id&lt;/code&gt; of your host system with the  &lt;code&gt;user id&lt;/code&gt; of the docker container. It is common that your local user and group have the id  &lt;code&gt;1000&lt;/code&gt;. In this case you would add the entries manually to the &lt;code&gt;.make/.env&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_USER_ID=1000
APP_GROUP_ID=1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See also section Solving permission issues.&lt;/p&gt;

&lt;p&gt;&lt;a id="enforce-required-parameters"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce required parameters
&lt;/h3&gt;

&lt;p&gt;We kinda "abuse" make for executing arbitrary commands (instead of building artifacts) and some of those commands require parameters that can be &lt;a href="https://stackoverflow.com/a/2826178/413531" rel="noopener noreferrer"&gt;passed as command arguments&lt;/a&gt; in the form&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make some-target FOO=bar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no way to "define" those parameters as we would in a method signature - but we can still ensure to fail as early as possible if a parameter is missing via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;FOO&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error FOO is empty or undefined&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See also &lt;a href="https://stackoverflow.com/a/10858332/413531" rel="noopener noreferrer"&gt;SO: How to abort makefile if variable not set?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We use this technique for example to ensure that all required variables are defined when we execute docker targets via the &lt;code&gt;validate-docker-variables&lt;/code&gt; precondition target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;validate-docker-variables&lt;/span&gt;
&lt;span class="nl"&gt;validate-docker-variables&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; 
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;TAG&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error TAG is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;ENV&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error ENV is undefined&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_REGISTRY&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error DOCKER_REGISTRY is undefined - Did you run make-init?&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_NAMESPACE&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error DOCKER_NAMESPACE is undefined - Did you run make-init?&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error APP_USER_NAME is undefined - Did you run make-init?&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;@$(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_GROUP_NAME&lt;span class="p"&gt;)&lt;/span&gt;,,&lt;span class="p"&gt;$(&lt;/span&gt;error APP_GROUP_NAME is undefined - Did you run make-init?&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;docker-build-image&lt;/span&gt;
&lt;span class="nl"&gt;docker-build-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;validate-docker-variables&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; build &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="make-docker-3"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make + Docker = &amp;lt;3
&lt;/h2&gt;

&lt;p&gt;We already introduced quite some complexity into our setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"global" variables (shared between &lt;code&gt;make&lt;/code&gt; and &lt;code&gt;docker&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;multiple &lt;code&gt;docker-compose.yml&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;build dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bringing it all together "manually" is quite an effort and prone to errors. But we can nicely tuck the complexity away in &lt;code&gt;.make/02-00-docker.mk&lt;/code&gt; by defining the two variables &lt;code&gt;DOCKER_COMPOSE&lt;/code&gt; and &lt;code&gt;DOCKER_COMPOSE_PHP_BASE&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;DOCKER_DIR&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;./.docker
&lt;span class="nv"&gt;DOCKER_ENV_FILE&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_DIR&lt;span class="p"&gt;)&lt;/span&gt;/.env
&lt;span class="nv"&gt;DOCKER_COMPOSE_DIR&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose
&lt;span class="nv"&gt;DOCKER_COMPOSE_FILE&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose.yml
&lt;span class="nv"&gt;DOCKER_COMPOSE_FILE_LOCAL&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose.local.yml
&lt;span class="nv"&gt;DOCKER_COMPOSE_FILE_PHP_BASE&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_DIR&lt;span class="p"&gt;)&lt;/span&gt;/docker-compose-php-base.yml
&lt;span class="nv"&gt;DOCKER_COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;dofroscra_&lt;span class="p"&gt;$(&lt;/span&gt;ENV&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;DOCKER_COMPOSE_COMMAND&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="nv"&gt;ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;ENV&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;TAG&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nv"&gt;DOCKER_REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_REGISTRY&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nv"&gt;DOCKER_NAMESPACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_NAMESPACE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nv"&gt;APP_USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nv"&gt;APP_GROUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;APP_GROUP_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 docker-compose &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_PROJECT_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--env-file&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_ENV_FILE&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;DOCKER_COMPOSE&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_COMMAND&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE_LOCAL&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_COMPOSE_PHP_BASE&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_COMMAND&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_FILE_PHP_BASE&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_COMPOSE&lt;/code&gt; uses &lt;code&gt;docker-compose.yml&lt;/code&gt; and extends it with &lt;code&gt;docker-compose.local.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_COMPOSE_PHP_BASE&lt;/code&gt; uses only &lt;code&gt;docker-compose-php-base.yml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The variables can then be used later in make recipes.&lt;/p&gt;

&lt;p&gt;&lt;a id="ensuring-the-build-order"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Ensuring the build order
&lt;/h3&gt;

&lt;p&gt;As mentioned under PHP images, we &lt;strong&gt;need to build images in a certain order&lt;/strong&gt; and use the following make targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;docker-build-image&lt;/span&gt;
&lt;span class="nl"&gt;docker-build-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Build all docker images OR a specific image by providing the service name via: make docker-build DOCKER_SERVICE_NAME=&amp;lt;service&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; build &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;docker-build-php&lt;/span&gt;
&lt;span class="nl"&gt;docker-build-php&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Build the php base image&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE_PHP_BASE&lt;span class="p"&gt;)&lt;/span&gt; build &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME_PHP_BASE&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;docker-build&lt;/span&gt;
&lt;span class="nl"&gt;docker-build&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;docker-build-php docker-build-image &lt;/span&gt;&lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Build the php image and then all other docker images&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a developer, I can now simply run &lt;code&gt;make docker-build&lt;/code&gt; - which will first build the &lt;code&gt;php-base&lt;/code&gt;  image via &lt;code&gt;docker-build-php&lt;/code&gt; and then build all the remaining images via &lt;code&gt;docker-build-image&lt;/code&gt;  (by not specifying the &lt;code&gt;DOCKER_SERVICE_NAME&lt;/code&gt; variable, &lt;code&gt;docker-compose&lt;/code&gt; will build &lt;strong&gt;all&lt;/strong&gt; services  listed in the &lt;code&gt;docker-compose.yml&lt;/code&gt; files).&lt;/p&gt;

&lt;p&gt;I would argue that the &lt;strong&gt;make recipes themselves are quite readable&lt;/strong&gt; and easy to understand but when  we run them with the &lt;a href="https://www.gnu.org/software/make/manual/html_node/Options-Summary.html" rel="noopener noreferrer"&gt;&lt;code&gt;-n&lt;/code&gt; option&lt;/a&gt; to only "Print the recipe that would be executed, but not execute it", we get a feeling for the complexity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make docker-build -n
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose-php-base.yml build php-base
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="run-commands-in-the-docker-containers"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run commands in the docker containers
&lt;/h3&gt;

&lt;p&gt;Tooling is an important part in the development workflow. This includes things like linters, static analyzers and testing tools but also "custom" tools geared towards your specific workflow. Those tools usually &lt;strong&gt;require a PHP runtime&lt;/strong&gt;. For now, we only have a single "tool" defined in the file &lt;code&gt;setup.php&lt;/code&gt;. It ensures that a table called &lt;code&gt;jobs&lt;/code&gt; is created.&lt;/p&gt;

&lt;p&gt;To run this tool, we must first start the docker setup via &lt;code&gt;make docker-up&lt;/code&gt; and then execute the script in the &lt;code&gt;application&lt;/code&gt; container. The corresponding target is defined in &lt;code&gt;.make/01-00-application-setup.mk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;setup-db&lt;/span&gt;
&lt;span class="nl"&gt;setup-db&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;##&lt;/span&gt;&lt;span class="nf"&gt; Setup the DB tables&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;span class="p"&gt;)&lt;/span&gt; php setup.php &lt;span class="p"&gt;$(&lt;/span&gt;ARGS&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which essentially translates to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose exec -T --user application application php setup.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;if we are outside of a container and to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php setup.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;if we are inside a container. That's quite handy, because we can &lt;strong&gt;run the tooling directly from the host system&lt;/strong&gt; without having to log into a container.&lt;/p&gt;

&lt;p&gt;The "magic" happens in the &lt;code&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/code&gt; variable that is defined in &lt;code&gt;.make/02-00-docker.mk&lt;/code&gt; as&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;EXECUTE_IN_WORKER_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;
&lt;span class="nv"&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;

&lt;span class="nv"&gt;EXECUTE_IN_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;?=&lt;/span&gt;
&lt;span class="k"&gt;ifndef&lt;/span&gt; &lt;span class="nv"&gt;EXECUTE_IN_CONTAINER&lt;/span&gt;
    &lt;span class="c"&gt;# check if 'make' is executed in a docker container, 
&lt;/span&gt; &lt;span class="c"&gt;# see https://stackoverflow.com/a/25518538/413531
&lt;/span&gt; &lt;span class="c"&gt;# `wildcard $file` checks if $file exists, 
&lt;/span&gt; &lt;span class="c"&gt;# see https://www.gnu.org/software/make/manual/html_node/Wildcard-Function.html
&lt;/span&gt; &lt;span class="c"&gt;# i.e. if the result is "empty" then $file does NOT exist 
&lt;/span&gt; &lt;span class="c"&gt;# =&amp;gt; we are NOT in a container
&lt;/span&gt; &lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;("$(wildcard /.dockerenv)","")&lt;/span&gt;
        &lt;span class="nv"&gt;EXECUTE_IN_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;endif&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;span class="k"&gt;ifeq&lt;/span&gt; &lt;span class="nv"&gt;($(EXECUTE_IN_CONTAINER),true)&lt;/span&gt;
    &lt;span class="nv"&gt;EXECUTE_IN_APPLICATION_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME_APPLICATION&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;EXECUTE_IN_WORKER_CONTAINER&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_COMPOSE&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;APP_USER_NAME&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;DOCKER_SERVICE_NAME_PHP_WORKER&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can take a look via &lt;code&gt;-n&lt;/code&gt; again to see the resolved recipe on the host system&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pascal.landau:/c/_codebase/dofroscra# make setup-db ARGS=--drop -n
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application application php setup.php --drop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within a container it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root:/var/www/app# make setup-db ARGS=--drop -n
php setup.php --drop;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="solving-permission-issues"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Solving permission issues
&lt;/h4&gt;

&lt;p&gt;If you are using Linux, you &lt;strong&gt;might run into permission issues when modifying files&lt;/strong&gt; that are  shared between the host system and the docker containers &lt;strong&gt;when the user id is not the same&lt;/strong&gt; as  explained in section  &lt;a href="https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#synchronizing-file-and-folder-ownership-on-shared-volumes" rel="noopener noreferrer"&gt;Synchronizing file and folder ownership on shared volumes&lt;/a&gt;  of the previous tutorial.&lt;/p&gt;

&lt;p&gt;In this case, you need to modify the &lt;code&gt;.make/.env&lt;/code&gt; manually and add the &lt;code&gt;APP_USER_ID&lt;/code&gt; and &lt;code&gt;APP_GROUP_ID&lt;/code&gt; variables according to your local setup. This &lt;strong&gt;must be done &lt;em&gt;before&lt;/em&gt; building the images&lt;/strong&gt; to ensure that the correct &lt;code&gt;user id&lt;/code&gt; is used in the images.&lt;/p&gt;

&lt;p&gt;In very rare cases it can lead to problems, because &lt;strong&gt;your local ids will &lt;em&gt;already exist&lt;/em&gt; in  the docker containers&lt;/strong&gt;. I've personally never run into this problem, but you can read about it  in more detail at  &lt;a href="https://www.joyfulbikeshedding.com/blog/2021-03-15-docker-and-the-host-filesystem-owner-matching-problem.html" rel="noopener noreferrer"&gt;Docker and the host filesystem owner matching problem&lt;/a&gt;. The author &lt;a href="https://twitter.com/honglilai/status/1509781424345432064" rel="noopener noreferrer"&gt;even proposes a general solution&lt;/a&gt; via &lt;a href="https://github.com/FooBarWidget/matchhostfsowner" rel="noopener noreferrer"&gt;the Github project "FooBarWidget/matchhostfsowner"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id="php-poc"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP POC
&lt;/h2&gt;

&lt;p&gt;To ensure that everything works as expected, the repository contains a minimal PHP proof of concept. By default, port 80 from the host ist forwarded to port 80 of the &lt;code&gt;nginx&lt;/code&gt; container.&lt;/p&gt;

&lt;p&gt;FYI: I would also recommend to add the following entry &lt;a href="https://www.howtogeek.com/howto/27350/beginner-geek-how-to-edit-your-hosts-file/" rel="noopener noreferrer"&gt;in the hosts file on the host machine&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.0.1 app.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so that we can access the application via &lt;a href="http://app.local" rel="noopener noreferrer"&gt;http://app.local&lt;/a&gt; instead of &lt;a href="http://127.0.0.1" rel="noopener noreferrer"&gt;http://127.0.0.1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The files of the POC essentially ensure that the container connections outlined in  Local docker setup work as expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pascallandau.com/img/docker-from-scratch-for-php-applications-in-2022/docker-containers.PNG" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.pascallandau.com%2Fimg%2Fdocker-from-scratch-for-php-applications-in-2022%2Fdocker-containers.PNG" title="Docker container connections" alt="Docker container connections"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dependencies.php&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;returns configured &lt;code&gt;Redis&lt;/code&gt; and &lt;code&gt;PDO&lt;/code&gt; objects to talk to the queue and the database&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;setup.php&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;=&amp;gt; &lt;em&gt;ensures that &lt;code&gt;application&lt;/code&gt; can talk to &lt;code&gt;mysql&lt;/code&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;public/index.php&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;is the web root file that can be accessed via &lt;a href="http://app.local" rel="noopener noreferrer"&gt;http://app.local&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;=&amp;gt; &lt;em&gt;ensures that &lt;code&gt;nginx&lt;/code&gt; and &lt;code&gt;php-fpm&lt;/code&gt; are working&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;contains 3 different "routes":

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://app.local?dispatch=some-job-id" rel="noopener noreferrer"&gt;http://app.local?dispatch=some-job-id&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;dispatches a new "job" with the id &lt;code&gt;some-job-id&lt;/code&gt; on the queue to be picked up by a 
worker

&lt;ul&gt;
&lt;li&gt;=&amp;gt; &lt;em&gt;ensures that &lt;code&gt;php-fpm&lt;/code&gt; can talk to &lt;code&gt;redis&lt;/code&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

&lt;a href="http://app.local?queue" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="http://app.local?queue" rel="noopener noreferrer"&gt;http://app.local?queue&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;shows the content of the queue&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="http://app.local?db" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="http://app.local?db" rel="noopener noreferrer"&gt;http://app.local?db&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;shows the content of the database

&lt;ul&gt;
&lt;li&gt;=&amp;gt; &lt;em&gt;ensures that &lt;code&gt;php-fpm&lt;/code&gt; can talk to &lt;code&gt;mysql&lt;/code&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;worker.php&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;is started as daemon process in the &lt;code&gt;php-worker&lt;/code&gt; container&lt;/li&gt;
&lt;li&gt;checks the redis datasbase &lt;code&gt;0&lt;/code&gt; for the key &lt;code&gt;"queue"&lt;/code&gt; every second&lt;/li&gt;
&lt;li&gt;if a value is found it is stored in the &lt;code&gt;jobs&lt;/code&gt; table of the database

&lt;ul&gt;
&lt;li&gt;=&amp;gt; &lt;em&gt;ensures that &lt;code&gt;php-worker&lt;/code&gt; can talk to &lt;code&gt;redis&lt;/code&gt; and &lt;code&gt;mysql&lt;/code&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;A full test scenario is defined in &lt;code&gt;test.sh&lt;/code&gt; and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bash test.sh


  Building the docker setup


//...


  Starting the docker setup


//...


  Clearing DB


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application application php setup.php --drop;
Dropping table 'jobs'
Done
Creating table 'jobs'
Done


  Stopping workers


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker supervisorctl stop worker:*;
worker:worker_00: stopped
worker:worker_01: stopped
worker:worker_02: stopped
worker:worker_03: stopped


  Ensuring that queue and db are empty


Items in queue
array(0) {
}
Items in db
array(0) {
}


  Dispatching a job 'foo'


Adding item 'foo' to queue


  Asserting the job 'foo' is on the queue


Items in queue
array(1) {
  [0]=&amp;gt;
  string(3) "foo"
}


  Starting the workers


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker supervisorctl start worker:*;
worker:worker_00: started
worker:worker_01: started
worker:worker_02: started
worker:worker_03: started


  Asserting the queue is now empty


Items in queue
array(0) {
}


  Asserting the db now contains the job 'foo'


Items in db
array(1) {
  [0]=&amp;gt;
  string(3) "foo"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="wrapping-up"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. Apart from that, you should now have a running docker setup and the means to  "control" it conveniently via &lt;code&gt;make&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the next part of this tutorial, we will  &lt;a href="https://www.pascallandau.com/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/" rel="noopener noreferrer"&gt;configure PhpStorm as our IDE to use the docker setup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Please subscribe to the &lt;a href="https://www.pascallandau.com/feed.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt; or &lt;a href="https://www.pascallandau.com/blog/#newsletter" rel="noopener noreferrer"&gt;via email&lt;/a&gt; to get automatic notifications when this next part comes out :)&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>php</category>
    </item>
  </channel>
</rss>
