diff --git a/.ahoy.yml b/.ahoy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a763df4db04552dff37f1f9b78d31331f86efa7a
--- /dev/null
+++ b/.ahoy.yml
@@ -0,0 +1,14 @@
+ahoyapi: v2
+commands:
+  d4d:
+    imports:
+      - ahoy.yml
+    usage: Docker for Drupal
+  live:
+    imports:
+      - ahoy.live.yml
+    usage: Interact with the live site
+  debug:
+    imports:
+      - ahoy.debug.yml
+    usage: PHP debugging
diff --git a/composer.json b/composer.json
index f06714d52e487f9a48de3b8dcb21128d3bb0e497..362df9b486428c61f478be4f3ab94b4a296f79be 100644
--- a/composer.json
+++ b/composer.json
@@ -27,10 +27,8 @@
         "ext-json": "*",
         "php": ">=7.2",
         "composer-plugin-api": "^1||^2",
-        "lakedrops/composer-json-utils": "^1.5.0",
-        "lakedrops/docker-traefik": "^1.2.0",
-        "lakedrops/dotenv": "^1.2.2",
-        "twig/twig": "^1.23.1"
+        "lakedrops/composer-json-utils": "^1.7||dev-master",
+        "lakedrops/docker-traefik": "^1.3||dev-master"
     },
     "require-dev": {
         "composer/composer": "^1||^2",
@@ -44,22 +42,6 @@
         }
     },
     "extra": {
-        "class": "LakeDrops\\Docker4Drupal\\Plugin",
-        "lakedrops": {
-            "ahoy": {
-                "d4d": {
-                    "usage": "Docker for Drupal",
-                    "imports": ["ahoy.yml"]
-                },
-                "live": {
-                    "usage": "Interact with the live site",
-                    "imports": ["ahoy.live.yml"]
-                },
-                "debug": {
-                    "usage": "PHP debugging",
-                    "imports": ["ahoy.debug.yml"]
-                }
-            }
-        }
+        "class": "LakeDrops\\Docker4Drupal\\Plugin"
     }
 }
diff --git a/src/Handler.php b/src/Handler.php
index 5709ec4426304782d5ac0047c09c10d5e4e8b4a4..43278c3eecd2e9553eeda0c1f17959ac40b3ff51 100644
--- a/src/Handler.php
+++ b/src/Handler.php
@@ -4,12 +4,9 @@ namespace LakeDrops\Docker4Drupal;
 
 use Exception;
 use LakeDrops\Component\Composer\BaseHandler;
-use LakeDrops\Component\Dotenv\Dotenv;
 use LakeDrops\DockerTraefik\Traefik;
 use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\Yaml\Yaml;
-use Twig_Environment;
-use Twig_Loader_Array;
 
 /**
  * Class Handler.
@@ -19,36 +16,154 @@ use Twig_Loader_Array;
 class Handler extends BaseHandler {
 
   /**
-   * @var array
+   * {@inheritdoc}
    */
-  protected $options;
+  public function configId(): string {
+    return 'docker4drupal';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configDefault(): array {
+    $projectname = getenv('COMPOSE_PROJECT_NAME');
+    if (empty($projectname)) {$projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd()));
+      $this->env->put('COMPOSE_PROJECT_NAME', $projectname);
+    }
+
+    $options = [
+      'projectname' => $projectname,
+      'ci_home' => '/home/gitlab-runner',
+      'docker0' => [
+        'ip' => ($this->isCiContext() || $this->isLocalDevMode()) ?
+          $this->getDockerGateway() :
+          $this->getLocalIpv4('docker0'),
+      ],
+      'live' => [
+        'root' => '',
+        'uri' => '',
+        'host' => '',
+        'user' => $this->env->receive('live_host_username', 'Remote username for host of the live site', getenv('USER')),
+      ],
+      'drush' => [
+        'sql' => [
+          'tables' => [
+            'structure' => [
+              'cache',
+              'cachetags',
+              'cache_*',
+              'history',
+              'search_*',
+              'sessions',
+              'watchdog',
+            ],
+            'skip' => [
+              'migration_*',
+            ],
+          ],
+        ],
+      ],
+      'drupal' => [
+        'version' => '8',
+      ],
+      'php' => [
+        'version' => $this->env->receiveGlobal('PHP_VERSION', 'PHP version', '7.3'),
+        'xdebug' => $this->env->receiveGlobal('PHP_DEBUG', 'PHP debug', '0'),
+      ],
+      'webserver' => [
+        'type' => 'apache',
+        'overwriteconfig' => FALSE,
+      ],
+      'varnish' => [
+        'enable' => 0,
+      ],
+      'redis' => [
+        'version' => '4.0',
+      ],
+      'dbbrowser' => [
+        'type' => 'pma',
+      ],
+      'solr' => [
+        'enable' => 0,
+        'version' => '6.6',
+      ],
+      'node' => [
+        'enable' => 0,
+        'key' => '',
+        'path' => '',
+      ],
+      'memcached' => [
+        'enable' => 0,
+      ],
+      'rsyslog' => [
+        'enable' => 0,
+      ],
+      'athenapdf' => [
+        'enable' => 0,
+        'key' => '',
+      ],
+      'blackfire' => [
+        'enable' => 0,
+        'id' => '',
+        'token' => '',
+      ],
+      'webgrind' => [
+        'enable' => 0,
+      ],
+      'wkhtmltox' => [
+        'enable' => 0,
+      ],
+    ];
+
+    if ($this->isCiContext() || $this->isLocalDevMode()) {
+      $projectRoot = $this->getDockerMountSource(getenv('CI_PROJECT_DIR'));
+    }
+    else {
+      $projectRoot = getcwd();
+    }
+
+    // Check if SSH auth sockets are supported.
+    $ssh_auth_sock = getenv('SSH_AUTH_SOCK');
+    $options['php']['ssh'] = !empty($ssh_auth_sock);
+    if ($options['php']['ssh']) {
+      $options['php']['ssh_auth_sock'] = ($this->isCiContext() || $this->isLocalDevMode()) ?
+        $this->getDockerMountSource('/ssh-agent') :
+        '$SSH_AUTH_SOCK';
+    }
+
+    $options['projectroot'] = $projectRoot;
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function postInit(): void {
+    $this->env->put('PHP_VERSION', $this->config->readValue('php/version'), TRUE);
+  }
+
 
   /**
    * Configure Drupal Project for Docker.
    *
    * @param bool $overwrite
    *   Whether to overwrite existing config files.
-   *
-   * @throws \Twig\Error\LoaderError
-   * @throws \Twig\Error\RuntimeError
-   * @throws \Twig\Error\SyntaxError
    */
-  public function configureProject($overwrite = FALSE) {
+  public function configureProject($overwrite = FALSE): void {
 
     // We only do the fancy stuff for developers.
     if (!$this->isDevMode()) {
       return;
     }
 
-    $options = $this->getOptions();
     $fs = new Filesystem();
     $installationManager = $this->composer->getInstallationManager();
 
-    if (isset($options['webroot'])) {
-      if (!$fs->exists($options['webroot'])) {
+    $webRoot = $this->config->readValue('webroot');
+    if ($webRoot !== NULL) {
+      if (!$fs->exists($webRoot)) {
         return;
       }
-      $webRoot = $options['webroot'];
     }
     else {
       $drupalCorePackage = $this->getDrupalCorePackage();
@@ -78,33 +193,27 @@ class Handler extends BaseHandler {
     }
 
     // Provide all the required files.
-    $twig_loader = new Twig_Loader_Array([]);
-    $twig = new Twig_Environment($twig_loader);
-    $options['webRoot'] = $webRoot . '/';
     $orig_ignored = FALSE;
-    foreach ($this->getFiles($projectRoot, $webRoot, $settingsPath, $options) as $template => $def) {
+    foreach ($this->getFiles($projectRoot, $webRoot, $settingsPath) as $template => $def) {
       if (isset($def['condition']) && !$def['condition']) {
         continue;
       }
       if (!$fs->exists($def['dest'])) {
         $fs->mkdir($def['dest']);
       }
-      $twig_loader->setTemplate($template, $template);
-      $filename = $twig->render($template, $options);
+      $filename = $this->config->render($template, $template);
       $file = $def['dest'] . '/' . $filename;
       if (($overwrite && empty($def['add2git'])) || !$fs->exists($file)) {
-        $twig_loader->setTemplate($filename, file_get_contents($pluginRoot . '/templates/' . ($def['source'] ?? '') . $template . '.twig'));
-        $rendered = $twig->render($filename, $options);
-        if (!empty($def['add2yaml']) && isset($options[$filename])) {
+        $rendered = $this->config->render($filename, file_get_contents($pluginRoot . '/templates/' . ($def['source'] ?? '') . $template . '.twig'));
+        $extraOptions = $this->config->readValue($filename);
+        if (!empty($def['add2yaml']) && $extraOptions !== NULL) {
           $yaml = Yaml::parse($rendered);
           /** @noinspection SlowArrayOperationsInLoopInspection */
-          $yaml = array_merge_recursive($yaml, $options[$filename]);
+          $yaml = array_merge_recursive($yaml, $extraOptions);
           $rendered = Yaml::dump($yaml, 9, 2);
 
           // Render the string again so that custom content can also use variables
-          $twig_loader->setTemplate($filename, $rendered);
-          $rendered = $twig->render($filename, $options);
-
+          $rendered = $this->config->render($filename, $rendered);
         }
         if ($fs->exists($file)) {
           if (md5_file($file) === md5($rendered)) {
@@ -150,7 +259,7 @@ class Handler extends BaseHandler {
     $this->gitIgnore('tests/backstop/backstop_data/html_report');
     $this->gitLFS('tests/backstop/**/*.png');
 
-    $traefik = new Traefik($options['projectname']);
+    $traefik = new Traefik($this->config->readValue('projectname'));
     $traefik->update();
 
     // Set permissions, see https://wodby.com/stacks/drupal/docs/local/permissions
@@ -167,13 +276,11 @@ class Handler extends BaseHandler {
    *   Name of the web's root directory.
    * @param string $settingsPath
    *   Name of the settings directory.
-   * @param array $options
-   *   Keyed array with all current options.
    *
    * @return array
    *   List of files.
    */
-  protected function getFiles($projectRoot, $webRoot, $settingsPath, $options): array {
+  protected function getFiles(string $projectRoot, string $webRoot, string $settingsPath): array {
     return [
       'settings.docker.php' => [
         'dest' => $projectRoot . '/' . $settingsPath,
@@ -205,139 +312,11 @@ class Handler extends BaseHandler {
       ],
       'vhost.conf' => [
         'dest' => $projectRoot . '/apache',
-        'condition' => $options['webserver']['overwriteconfig'],
+        'condition' => $this->config->readValue('webserver/overwriteconfig'),
       ],
     ];
   }
 
-  /**
-   * Retrieve options from composer.json "extra" configuration.
-   *
-   * @param null $key
-   *   Optional name of an option to be received instead of the full set of options.
-   *
-   * @return array|string
-   *   The options.
-   */
-  public function getOptions($key = NULL) {
-    if ($this->options === NULL) {
-      $env = new Dotenv('docker4drupal', $this->io);
-      $projectname = getenv('COMPOSE_PROJECT_NAME');
-      if (empty($projectname)) {$projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd()));
-        $env->put('COMPOSE_PROJECT_NAME', $projectname);
-      }
-      $extra = $this->composer->getPackage()->getExtra() + ['docker4drupal' => []];
-      $options = NestedArray::mergeDeep([
-        'projectname' => $projectname,
-        'ci_home' => '/home/gitlab-runner',
-        'docker0' => [
-          'ip' => ($this->isCiContext() || $this->isLocalDevMode()) ?
-            $this->getDockerGateway() :
-            $this->getLocalIpv4('docker0'),
-        ],
-        'live' => [
-          'root' => '',
-          'uri' => '',
-          'host' => '',
-          'user' => $env->receive('live_host_username', 'Remote username for host of the live site', getenv('USER')),
-        ],
-        'drush' => [
-          'sql' => [
-            'tables' => [
-              'structure' => [
-                'cache',
-                'cachetags',
-                'cache_*',
-                'history',
-                'search_*',
-                'sessions',
-                'watchdog',
-              ],
-              'skip' => [
-                'migration_*',
-              ],
-            ],
-          ],
-        ],
-        'drupal' => [
-          'version' => '8',
-        ],
-        'php' => [
-          'version' => $env->receiveGlobal('PHP_VERSION', 'PHP version', '7.3'),
-          'xdebug' => $env->receiveGlobal('PHP_DEBUG', 'PHP debug', '0'),
-        ],
-        'webserver' => [
-          'type' => 'apache',
-          'overwriteconfig' => FALSE,
-        ],
-        'varnish' => [
-          'enable' => 0,
-        ],
-        'redis' => [
-          'version' => '4.0',
-        ],
-        'dbbrowser' => [
-          'type' => 'pma',
-        ],
-        'solr' => [
-          'enable' => 0,
-          'version' => '6.6',
-        ],
-        'node' => [
-          'enable' => 0,
-          'key' => '',
-          'path' => '',
-        ],
-        'memcached' => [
-          'enable' => 0,
-        ],
-        'rsyslog' => [
-          'enable' => 0,
-        ],
-        'athenapdf' => [
-          'enable' => 0,
-          'key' => '',
-        ],
-        'blackfire' => [
-          'enable' => 0,
-          'id' => '',
-          'token' => '',
-        ],
-        'webgrind' => [
-          'enable' => 0,
-        ],
-        'wkhtmltox' => [
-          'enable' => 0,
-        ],
-      ], $extra['docker4drupal']);
-
-      if ($this->isCiContext() || $this->isLocalDevMode()) {
-        $projectRoot = $this->getDockerMountSource(getenv('CI_PROJECT_DIR'));
-      }
-      else {
-        $projectRoot = getcwd();
-      }
-
-      // Check if SSH auth sockets are supported.
-      $ssh_auth_sock = getenv('SSH_AUTH_SOCK');
-      $options['php']['ssh'] = !empty($ssh_auth_sock);
-      if ($options['php']['ssh']) {
-        $options['php']['ssh_auth_sock'] = ($this->isCiContext() || $this->isLocalDevMode()) ?
-          $this->getDockerMountSource('/ssh-agent') :
-          '$SSH_AUTH_SOCK';
-      }
-
-      $options['projectroot'] = $projectRoot;
-
-    $this->options = $env->replaceEnvironmentVariables($options);
-      $env->put('PHP_VERSION', $this->options['php']['version'], TRUE);
-    }
-    if ($key !== NULL) {
-      return $this->options[$key];
-    }
-    return $this->options;
-  }
-
   /**
    * Determine local ipv4 address.
    *
@@ -399,7 +378,10 @@ class Handler extends BaseHandler {
     return getcwd();
   }
 
-  private function readContainerConfig() {
+  /**
+   * @return array
+   */
+  private function readContainerConfig(): array {
     try {
       $output = [];
       exec('basename "$(cat /proc/1/cpuset)"', $output);
diff --git a/src/NestedArray.php b/src/NestedArray.php
deleted file mode 100644
index b1be92357c732721d80a428be7f3161ffb418afb..0000000000000000000000000000000000000000
--- a/src/NestedArray.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-namespace LakeDrops\Docker4Drupal;
-
-/**
- * Class NestedArray.
- *
- * @package LakeDrops\Docker4Drupal
- */
-class NestedArray {
-
-  /**
-   * Deeply merges arrays. Borrowed from drupal.org/project/core.
-   *
-   * @return array
-   *   The merged array.
-   */
-  public static function mergeDeep(): array {
-    return self::mergeDeepArray(func_get_args());
-  }
-
-  /**
-   * Deeply merges arrays. Borrowed from drupal.org/project/core.
-   *
-   * @param array $arrays
-   *   An array of array that will be merged.
-   * @param bool $preserve_integer_keys
-   *   Whether to preserve integer keys.
-   *
-   * @return array
-   *   The merged array.
-   */
-  public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FALSE): array {
-    $result = [];
-    foreach ($arrays as $array) {
-      foreach ($array as $key => $value) {
-        if (is_int($key) && !$preserve_integer_keys) {
-          $result[] = $value;
-        }
-        /** @noinspection NotOptimalIfConditionsInspection */
-        elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
-          $result[$key] = self::mergeDeepArray([$result[$key], $value], $preserve_integer_keys);
-        }
-        else {
-          $result[$key] = $value;
-        }
-      }
-    }
-    return $result;
-  }
-
-}
diff --git a/src/Plugin.php b/src/Plugin.php
index 74e33259afd0f5453510e946423a4a66fa218b3b..2dc896cf9f103019b345f43b2ce192582255d8fe 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -4,6 +4,7 @@ namespace LakeDrops\Docker4Drupal;
 
 use Composer\Script\Event;
 use Composer\Script\ScriptEvents;
+use LakeDrops\Component\Composer\BaseHandlerInterface;
 use LakeDrops\Component\Composer\BasePlugin;
 
 /**
@@ -14,7 +15,7 @@ class Plugin extends BasePlugin {
   /**
    * {@inheritdoc}
    */
-  public function getHandlerClass() {
+  public function getHandlerClass(): BaseHandlerInterface {
     return Handler::class;
   }
 
diff --git a/src/UpdateCommand.php b/src/UpdateCommand.php
index 0f2664b5f963e5ab93e90635db2349830c354d35..c5d0e94d096c9836f40ce3669255a7a75d200124 100644
--- a/src/UpdateCommand.php
+++ b/src/UpdateCommand.php
@@ -3,6 +3,7 @@
 namespace LakeDrops\Docker4Drupal;
 
 use LakeDrops\Component\Composer\BaseCommand;
+use LakeDrops\Component\Composer\BaseHandlerInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
@@ -19,7 +20,7 @@ class UpdateCommand extends BaseCommand {
   /**
    * {@inheritdoc}
    */
-  public function getHandlerClass() {
+  public function getHandlerClass(): BaseHandlerInterface {
     return Handler::class;
   }
 
@@ -30,11 +31,12 @@ class UpdateCommand extends BaseCommand {
    * @throws \Twig\Error\RuntimeError
    * @throws \Twig\Error\SyntaxError
    */
-  protected function execute(InputInterface $input, OutputInterface $output) {
+  protected function execute(InputInterface $input, OutputInterface $output): int {
     parent::execute($input, $output);
     /** @var Handler $handler */
     $handler = $this->handler;
     $handler->configureProject(TRUE);
+    return 0;
   }
 
 }
diff --git a/templates/default.site.yml.twig b/templates/default.site.yml.twig
index 9c01c306042c183d117b0388b3218f4aeda63b6e..d6165651d9ad362fe7b639b970ebab52ce394e1b 100644
--- a/templates/default.site.yml.twig
+++ b/templates/default.site.yml.twig
@@ -1,8 +1,8 @@
 dev:
-  root: '/var/www/html/{{ webRoot }}'
+  root: '/var/www/html/{{ webRoot }}/'
   uri: '{{ projectname }}.docker.localhost:8000'
 behat:
-  root: '/var/www/html/{{ webRoot }}'
+  root: '/var/www/html/{{ webRoot }}/'
   uri: '{{ webserver.type }}'
 live:
   root: '{{ live.root }}'
diff --git a/templates/docker-compose.yml.twig b/templates/docker-compose.yml.twig
index 9848332c6cff8e0ed191de6b6fbf8b07c4ee8d6b..ed79cb16b294e9f66439ec3b8ba26b4c6b2c4e90 100644
--- a/templates/docker-compose.yml.twig
+++ b/templates/docker-compose.yml.twig
@@ -76,7 +76,7 @@ services:
 {% endif %}
 {% endif %}
       {{ webserver.type|upper }}_BACKEND_HOST: php
-      {{ webserver.type|upper }}_SERVER_ROOT: /var/www/html/{{ webRoot }}
+      {{ webserver.type|upper }}_SERVER_ROOT: /var/www/html/{{ webRoot }}/
     volumes:
       - {{ projectroot }}:/var/www/html
     labels:
diff --git a/templates/vhost.conf.twig b/templates/vhost.conf.twig
index 43460c420c4308755e9138ceca285a3e6d4e3b4c..07750ae83f2b3290b972c83e34191428596c8609 100644
--- a/templates/vhost.conf.twig
+++ b/templates/vhost.conf.twig
@@ -1,5 +1,5 @@
 <VirtualHost *:80>
-    DocumentRoot "/var/www/html/{{ webRoot }}"
+    DocumentRoot "/var/www/html/{{ webRoot }}/"
     ServerName default
     Include conf/preset.conf
     <Location />