diff --git a/src/building-build-configs-php-munich/code/generating-files.txt b/src/building-build-configs-php-munich/code/generating-files.txt
new file mode 100644
index 0000000..e69de29
diff --git a/src/building-build-configs-php-munich/diagram.png b/src/building-build-configs-php-munich/diagram.png
new file mode 100644
index 0000000..081415a
Binary files /dev/null and b/src/building-build-configs-php-munich/diagram.png differ
diff --git a/src/building-build-configs-php-munich/example.mp4 b/src/building-build-configs-php-munich/example.mp4
new file mode 100644
index 0000000..02f1d7b
Binary files /dev/null and b/src/building-build-configs-php-munich/example.mp4 differ
diff --git a/src/building-build-configs-php-munich/sections/conclusion.rst b/src/building-build-configs-php-munich/sections/conclusion.rst
new file mode 100644
index 0000000..985ec13
--- /dev/null
+++ b/src/building-build-configs-php-munich/sections/conclusion.rst
@@ -0,0 +1,27 @@
+.. raw:: pdf
+
+   PageBreak standardPage
+
+Result
+======
+
+- Easier and faster to create and onboard projects.
+- One canonical source of truth.
+- Easy to add new features and fixes for all projects.
+- Automation is easier due to consistency (e.g. Docker Compose service names).
+
+Thanks!
+=======
+
+References:
+
+- https://opdavi.es/build-configs
+- https://github.com/opdavies/docker-example-drupal
+- https://github.com/opdavies/docker-example-drupal-commerce-kickstart
+- https://github.com/opdavies/docker-example-drupal-localgov
+
+|
+
+Me:
+
+- https://www.oliverdavies.uk
diff --git a/src/building-build-configs-php-munich/sections/customisation.rst b/src/building-build-configs-php-munich/sections/customisation.rst
new file mode 100644
index 0000000..17324f7
--- /dev/null
+++ b/src/building-build-configs-php-munich/sections/customisation.rst
@@ -0,0 +1,52 @@
+.. raw:: pdf
+
+   PageBreak standardPage
+
+Overriding Values
+=================
+
+.. code-block:: yaml
+
+   php:
+     version: 8.1-fpm-bullseye
+     # Disable PHPCS, PHPStan and PHPUnit.
+     phpcs: false
+     phpstan: false
+     phpunit: false
+
+   # Ignore more directories from Git.
+   git:
+     ignore:
+       - /bin/
+       - /libraries/
+       - /web/profiles/contrib/
+
+.. raw:: pdf
+
+   TextAnnotation "Drupal Commerce Kickstart demo. No custom modules to test, and additional paths to ignore from Git."
+
+   PageBreak
+
+.. code-block:: yaml
+
+   dockerfile:
+     stages:
+       build:
+         # What additional directories do we need?
+         extra_directories:
+           - config
+           - patches
+           - scripts
+
+         commands:
+           - composer validate --strict
+           - composer install
+
+         # What additional PHP extensions do we need?
+         extensions:
+           install: [bcmath]
+
+.. raw:: pdf
+
+   TextAnnotation "Extra directories and PHP extensions that need to be added".
+
diff --git a/src/building-build-configs-php-munich/sections/example.rst b/src/building-build-configs-php-munich/sections/example.rst
new file mode 100644
index 0000000..6e8c115
--- /dev/null
+++ b/src/building-build-configs-php-munich/sections/example.rst
@@ -0,0 +1,92 @@
+Example
+=======
+
+build.yaml:
+
+.. code-block:: yaml
+
+   name: my-example-project
+   type: drupal
+   language: php
+
+   php:
+     version: 8.1-fpm-bullseye
+
+|
+
+Dockerfile:
+
+.. raw:: pdf
+
+   TextAnnotation "Abstract the project-specific values and configuration into this file."
+
+.. code-block:: yaml
+
+   FROM php:8.1-fpm-bullseye AS base
+
+Configuring a Project
+=====================
+
+.. code-block:: yaml
+
+   php:
+     version: 8.1-fpm-bullseye
+
+     # Which PHPCS standards should be used and on which paths?
+     phpcs:
+       paths: [web/modules/custom]
+       standards: [Drupal, DrupalPractice]
+
+     # What level should PHPStan run and on what paths?
+     phpstan:
+       level: max
+       paths: [web/modules/custom]
+
+.. raw:: pdf
+
+   PageBreak
+
+.. code-block:: yaml
+
+   docker-compose:
+     # Which Docker Compose services do we need?
+     services:
+       - database
+       - php
+       - web
+
+   dockerfile:
+     stages:
+       build:
+         # What commands do we need to run?
+         commands:
+           - composer validate --strict
+           - composer install
+
+.. raw:: pdf
+
+   PageBreak
+
+.. code-block:: yaml
+
+   web:
+     type: nginx # nginx, apache, caddy
+
+   database:
+     type: mariadb # mariadb, mysql
+     version: 10
+
+   # Where is Drupal located?
+   drupal:
+     docroot: web # web, docroot, null
+
+   experimental:
+     createGitHubActionsConfiguration: true
+     runGitHooksBeforePush: true
+     useNewDatabaseCredentials: true
+
+.. raw:: pdf
+
+  TextAnnotation "Experimental opt-in features that I want to trial on certain projects or to disable non-applicable features - e.g. GitHub Actions on Bitbucket."
+
+  PageBreak
diff --git a/src/building-build-configs-php-munich/sections/internals.rst b/src/building-build-configs-php-munich/sections/internals.rst
new file mode 100644
index 0000000..c007021
--- /dev/null
+++ b/src/building-build-configs-php-munich/sections/internals.rst
@@ -0,0 +1,300 @@
+.. raw:: pdf
+
+   PageBreak titlePage
+
+.. class:: centredtitle
+
+Build Configs internals
+
+.. raw:: pdf
+
+   PageBreak standardPage
+
+.. code-block::
+
+   src/
+       Action/
+           CreateFinalConfigurationData.php
+           CreateListOfFilesToGenerate.php
+           GenerateConfigurationFiles.php
+           ValidateConfigurationData.php
+       Command/
+           GenerateCommand.php
+           InitCommand.php
+       DataTransferObject/
+           ConfigDto.php
+           TemplateFile.php
+       Enum/
+           Language.php
+           ProjectType.php
+           WebServer.php
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   protected function configure(): void
+       $this
+           ->addOption(
+               name: 'config-file',
+               shortcut: ['c'],
+               mode: InputOption::VALUE_REQUIRED,
+               description: 'The path to the project\'s build.yaml file',
+               default: 'build.yaml',
+           )
+           ->addOption(
+               name: 'output-dir',
+               shortcut: ['o'],
+               mode: InputOption::VALUE_REQUIRED,
+               description: 'The directory to create files in',
+               default: '.',
+           );
+   }
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   protected function execute(InputInterface $input, OutputInterface $output): int
+   {
+      $io = new SymfonyStyle($input, $output);
+
+      $configFile = $input->getOption(name: 'config-file');
+      $outputDir = $input->getOption(name: 'output-dir');
+   }
+
+.. raw:: pdf
+
+   PageBreak
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   protected function execute(InputInterface $input, OutputInterface $output): int
+   {
+      // ...
+
+      $pipelines = [
+          new CreateFinalConfigurationData(),
+ 
+          new ValidateConfigurationData(),
+ 
+          new CreateListOfFilesToGenerate(),
+ 
+          new GenerateConfigurationFiles(
+              $this->filesystem,
+              $this->twig,
+              $outputDir,
+          ),
+      ];
+    }
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   protected function execute(InputInterface $input, OutputInterface $output): int
+   {
+       // ...
+
+       /**
+        * @var Collection<int,TemplateFile> $generatedFiles
+        * @var ConfigDto $configurationData
+        */
+       [$configurationData, $generatedFiles] = (new Pipeline())
+           ->send($configFile)
+           ->through($pipelines)
+           ->thenReturn();
+
+       $io->info("Building configuration for {$configurationData->name}.");
+
+       $io->write('Generated files:');
+       $io->listing(static::getListOfFiles(filesToGenerate: $generatedFiles)->toArray());
+
+       return Command::SUCCESS;
+   }
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   // CreateFinalConfigurationData.php
+
+   public function handle(string $configFile, \Closure $next) {
+   {
+       $configurationData = Yaml::parseFile(filename: $configFile);
+
+       $configurationData = array_replace_recursive(
+           Yaml::parseFile(filename: __DIR__ . '/../../resources/build.defaults.yaml'),
+           $configurationData,
+       );
+
+       // ...
+
+       return $next($configurationData);
+   }
+
+.. raw:: pdf
+
+   PageBreak
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   // ValidateConfigurationData.php
+
+   public function handle(array $configurationData, \Closure $next)
+   {
+       // Convert the input to a configuration data object.
+       $normalizer = new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter());
+       $serializer = new Serializer([$normalizer], [new JsonEncoder()]);
+
+       $configurationDataDto = $serializer->deserialize(
+           json_encode($configurationData),
+           ConfigDto::class,
+           'json',
+       );
+
+       // ...
+   }
+
+.. raw:: pdf
+
+   PageBreak
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   // ValidateConfigurationData.php
+
+   public function handle(array $configurationData, \Closure $next)
+   {
+       // ...
+
+       $validator = Validation::createValidatorBuilder()
+           ->enableAnnotationMapping()
+           ->getValidator();
+       $violations = $validator->validate($configurationDataDto);
+
+       if (0 < $violations->count()) {
+           throw new \RuntimeException('Configuration is invalid.');
+       }
+
+       return $next([$configurationData, $configurationDataDto]);
+   }
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   // ConfigDto.php
+
+   #[Assert\Collection(
+        allowExtraFields: false,
+        fields: ['docroot' => new Assert\Choice([null, 'web', 'docroot'])],
+    )]
+    public array $drupal;
+
+    #[Assert\Collection([
+        'ignore' => new Assert\Optional([
+            new Assert\All([
+                new Assert\Type('string'),
+            ]),
+        ]),
+    ])]
+    public array $git;
+
+    #[Assert\Choice(choices: ['javascript', 'php', 'typescript'])]
+    public string $language;
+
+    #[Assert\NotBlank]
+    #[Assert\Type('string')]
+    public string $name;
+
+    #[Assert\Type('string')]
+    public string $projectRoot;
+
+    #[Assert\Choice(choices: [
+        'drupal',
+        'fractal',
+        'php-library',
+        'symfony',
+   ])]
+   public string $type;
+
+.. code-block:: php
+   :startinline: true
+   :linenos:
+
+   // CreateListOfFilesToGenerate.php
+
+   public function handle(array $configurationDataAndDto, \Closure $next)
+   {
+       /**
+         * @var ConfigDto $configDto,
+         * @var array<string,mixed> $configurationData
+         */
+        [$configurationData, $configDto] = $configurationDataAndDto;
+
+        /** @var Collection<int, TemplateFile> */
+        $filesToGenerate = collect();
+
+        // ...
+   }
+
+.. code-block:: php
+   :startinline: true
+   :linenos:
+
+   // CreateListOfFilesToGenerate.php
+
+   public function handle(array $configurationDataAndDto, \Closure $next)
+   {
+       // ...
+
+       if (!isset($configDto->php['phpunit']) || $configDto->php['phpunit'] !== false) {
+
+           $filesToGenerate->push(
+               new TemplateFile(
+                   data: 'drupal/phpunit.xml.dist',
+                   name: 'phpunit.xml.dist',
+               )
+           );
+       }
+
+       // ...
+
+       return $next([$configurationData, $configDto, $filesToGenerate]);
+   }
+
+.. code-block:: php
+   :linenos:
+   :startinline: true
+
+   // GenerateConfigurationFiles.php
+
+   public function handle(array $filesToGenerateAndConfigurationData, \Closure $next)
+   {
+       // ...
+
+       $filesToGenerate->each(function(TemplateFile $templateFile) use ($configurationData): void {
+           if ($templateFile->path !== null) {
+               if (!$this->filesystem->exists($templateFile->path)) {
+                   $this->filesystem->mkdir("{$this->outputDir}/{$templateFile->path}");
+               }
+           }
+
+           $sourceFile = "{$templateFile->data}.twig";
+
+           $outputFile = collect([$this->outputDir, $templateFile->path, $templateFile->name])
+               ->filter()->implode('/');
+
+           $this->filesystem->dumpFile($outputFile, $this->twig->render($sourceFile, $configurationData));
+       });
+
+       return $next([$configurationDataDto, $filesToGenerate]);
+   }
+
diff --git a/src/building-build-configs-php-munich/sections/intro.rst b/src/building-build-configs-php-munich/sections/intro.rst
new file mode 100644
index 0000000..83206bd
--- /dev/null
+++ b/src/building-build-configs-php-munich/sections/intro.rst
@@ -0,0 +1,51 @@
+.. raw:: pdf
+
+   PageBreak standardPage
+
+What is "Build Configs"?
+========================
+
+- Command-line tool.
+- Inspired by Workspace, name from the TheAltF4Stream.
+- Built with Symfony.
+- Creates and manages build configuration files.
+- Customisable per-project.
+- Drupal, PHP library, Fractal (TypeScript).
+- "Sprint zero in a box".
+
+What Problem Does it Solve?
+===========================
+
+- I work on multiple similar projects.
+- Different configuration values - e.g. ``web`` vs. ``docroot``.
+- Different versions of PHP, node, etc.
+- Different Docker Compose (``fpm`` vs. ``apache`` images).
+- Each project was separate.
+- Difficult to add new features and fix bugs across all projects.
+- Inconsistencies across projects.
+- Out of the box solutions didn't seem like the best fit.
+
+.. raw:: pdf
+
+    TextAnnotation "Multiple projects with similar but different configurations."
+    TextAnnotation ""
+    TextAnnotation "Out of the box solutions tend to focus on one technology, could be hard to customise, and usually had more than I nedeed."
+    TextAnnotation ""
+    TextAnnotation "Start small and build up instead of removing additional things."
+    TextAnnotation ""
+    TextAnnotation "More opportunities to learn the underlying technologies."
+
+How Does it Work?
+=================
+
+.. image:: diagram.png
+  :width: 18cm
+
+What Files Does it Generate?
+============================
+
+- Dockerfile, Docker Compose, Nix Flake, php.ini, NGINX default.conf.
+- ``run`` file.
+- PHPUnit, PHPCS, PHPStan.
+- GitHub Actions workflow.
+- Git hooks.
diff --git a/src/building-build-configs-php-munich/sections/templates.rst b/src/building-build-configs-php-munich/sections/templates.rst
new file mode 100644
index 0000000..b07ec59
--- /dev/null
+++ b/src/building-build-configs-php-munich/sections/templates.rst
@@ -0,0 +1,117 @@
+.. raw:: pdf
+
+   PageBreak standardPage
+
+Dockerfile.twig
+===============
+
+.. code-block:: twig
+  :linenos:
+
+    FROM php:{{ php.version }} AS base
+
+    COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
+    RUN which composer && composer -V
+
+    ARG DOCKER_UID=1000
+    ENV DOCKER_UID="${DOCKER_UID}"
+
+    WORKDIR {{ project_root }}
+
+    RUN adduser --disabled-password --uid "${DOCKER_UID}" app \
+      && chown app:app -R {{ project_root }}
+
+Dockerfile.twig
+===============
+
+.. code-block:: twig
+   :linenos:
+
+    {% if dockerfile.stages.build.extensions.install %}
+    RUN docker-php-ext-install
+      {{ dockerfile.stages.build.extensions.install|join(' ') }}
+    {% endif %}
+
+    COPY --chown=app:app phpunit.xml* ./
+
+    {% if dockerfile.stages.build.extra_files %}
+    COPY --chown=app:app {{ dockerfile.stages.build.extra_files|join(" ") }} ./
+    {% endif %}
+
+    {% for directory in dockerfile.stages.build.extra_directories %}
+    COPY --chown=app:app {{ directory }} {{ directory }}
+    {% endfor %}
+
+docker-compose.yaml.twig
+========================
+
+.. code-block:: twig
+   :linenos:
+
+    services:
+    {% if "web" in dockerCompose.services %}
+      web:
+        <<: [*default-proxy, *default-app]
+        build:
+          context: .
+          target: web
+        depends_on:
+          - php
+        profiles: [web]
+    {% endif %}
+
+phpstan.neon.dist.twig
+======================
+
+.. code-block:: twig
+   :linenos:
+
+    parameters:
+      level: {{ php.phpstan.level }}
+      excludePaths:
+        - *Test.php
+        - *TestBase.php
+      paths:
+        {% for path in php.phpstan.paths -%}
+        - {{ path }}
+        {%- endfor %}
+
+    {% if php.phpstan.baseline %}
+    includes:
+      - phpstan-baseline.neon
+    {% endif %}
+
+phpunit.xml.dist.twig
+=====================
+
+.. code-block:: twig
+   :linenos:
+
+    <phpunit
+      beStrictAboutChangesToGlobalState="true"
+      beStrictAboutOutputDuringTests="false"
+      beStrictAboutTestsThatDoNotTestAnything="true"
+      bootstrap="{{ drupal.docroot }}/core/tests/bootstrap.php"
+      cacheResult="false"
+      colors="true"
+      failOnWarning="true"
+      printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter"
+    >
+
+phpunit.xml.dist.twig
+=====================
+
+.. code-block:: twig
+   :linenos:
+
+    <testsuites>
+      <testsuite name="functional">
+        <directory>./{{ drupal.docroot }}/modules/custom/**/tests/**/Functional</directory>
+      </testsuite>
+      <testsuite name="kernel">
+        <directory>./{{ drupal.docroot }}/modules/custom/**/tests/**/Kernel</directory>
+      </testsuite>
+      <testsuite name="unit">
+        <directory>./{{ drupal.docroot }}/modules/custom/**/tests/**/Unit</directory>
+      </testsuite>
+    </testsuites>
diff --git a/src/building-build-configs-php-munich/slides.rst b/src/building-build-configs-php-munich/slides.rst
new file mode 100644
index 0000000..0f2dafb
--- /dev/null
+++ b/src/building-build-configs-php-munich/slides.rst
@@ -0,0 +1,30 @@
+.. footer:: @opdavies
+
+Building "Build Configs"
+########################
+
+.. class:: titleslideinfo
+
+Oliver Davies (@opdavies)
+
+|
+
+.. class:: titleslideinfo
+
+https://opdavi.es/bcm
+
+.. include:: ./sections/intro.rst
+.. include:: ./sections/example.rst
+.. include:: ./sections/customisation.rst
+.. include:: ./sections/templates.rst
+.. include:: ./sections/internals.rst
+
+.. raw:: pdf
+
+   PageBreak titlePage
+
+.. class:: centredtitle
+
+Demo
+
+.. include:: ./sections/conclusion.rst