From ac6a21704ac17909399d878560ba12787e0299c9 Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+studip@gmail.com>
Date: Wed, 17 Apr 2024 14:00:21 +0000
Subject: [PATCH] Rework, fixes #7

---
 .gitignore                                    |    5 +
 compiled/.gitkeep                             |    0
 composer.json                                 |   24 +
 composer.lock                                 | 1063 +++++++++++++++++
 {php => config/docker-files}/7.2-Dockerfile   |   10 +-
 {php => config/docker-files}/7.3-Dockerfile   |   10 +-
 {php => config/docker-files}/7.4-Dockerfile   |   10 +-
 {php => config/docker-files}/8.0-Dockerfile   |   10 +-
 {php => config/docker-files}/8.1-Dockerfile   |   10 +-
 {php => config/docker-files}/8.2-Dockerfile   |   10 +-
 {php => config/docker-files}/8.3-Dockerfile   |   10 +-
 config/nginx-php.conf                         |   11 -
 config/nginx-sites.conf                       |   78 --
 config/nginx.conf                             |   91 --
 data/index.php                                |    2 -
 docker-compose.yml                            |   75 --
 generate.sh                                   |    1 -
 lib/Commands/Compile.php                      |  233 ++++
 lib/Commands/Docker/Build.php                 |   17 +
 lib/Commands/Docker/Start.php                 |   18 +
 lib/Commands/Docker/Stop.php                  |   18 +
 lib/Commands/Init.php                         |   45 +
 lib/Commands/PHP/Disable.php                  |   55 +
 lib/Commands/PHP/Enable.php                   |   57 +
 lib/Commands/PHP/Port.php                     |   53 +
 lib/Commands/Sites/Add.php                    |  122 ++
 lib/Commands/Sites/Remove.php                 |   49 +
 lib/Commands/Sites/Show.php                   |   33 +
 lib/Config.php                                |   84 ++
 lib/Creators/DockerComposeConfiguration.php   |   70 ++
 lib/Creators/NginxConfiguration.php           |  109 ++
 lib/DockerComposeCommand.php                  |   34 +
 lib/SupportedPHPVersions.php                  |   35 +
 .../GetCompiledDockerComposeYMLPath.php       |   17 +
 studip-docker                                 |   32 +
 web/index.php                                 |   48 +
 36 files changed, 2256 insertions(+), 293 deletions(-)
 create mode 100644 compiled/.gitkeep
 create mode 100644 composer.json
 create mode 100644 composer.lock
 rename {php => config/docker-files}/7.2-Dockerfile (86%)
 rename {php => config/docker-files}/7.3-Dockerfile (86%)
 rename {php => config/docker-files}/7.4-Dockerfile (86%)
 rename {php => config/docker-files}/8.0-Dockerfile (86%)
 rename {php => config/docker-files}/8.1-Dockerfile (86%)
 rename {php => config/docker-files}/8.2-Dockerfile (86%)
 rename {php => config/docker-files}/8.3-Dockerfile (87%)
 delete mode 100644 config/nginx-php.conf
 delete mode 100644 config/nginx-sites.conf
 delete mode 100644 config/nginx.conf
 delete mode 100644 data/index.php
 delete mode 100644 docker-compose.yml
 delete mode 100755 generate.sh
 create mode 100644 lib/Commands/Compile.php
 create mode 100644 lib/Commands/Docker/Build.php
 create mode 100644 lib/Commands/Docker/Start.php
 create mode 100644 lib/Commands/Docker/Stop.php
 create mode 100644 lib/Commands/Init.php
 create mode 100644 lib/Commands/PHP/Disable.php
 create mode 100644 lib/Commands/PHP/Enable.php
 create mode 100644 lib/Commands/PHP/Port.php
 create mode 100644 lib/Commands/Sites/Add.php
 create mode 100644 lib/Commands/Sites/Remove.php
 create mode 100644 lib/Commands/Sites/Show.php
 create mode 100644 lib/Config.php
 create mode 100644 lib/Creators/DockerComposeConfiguration.php
 create mode 100644 lib/Creators/NginxConfiguration.php
 create mode 100644 lib/DockerComposeCommand.php
 create mode 100644 lib/SupportedPHPVersions.php
 create mode 100644 lib/Traits/GetCompiledDockerComposeYMLPath.php
 create mode 100755 studip-docker
 create mode 100644 web/index.php

diff --git a/.gitignore b/.gitignore
index a5fc231..fb44e82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
+.idea
+compiled/*
 logs/*/*.log
+vendor/*
+
+config.json
 
diff --git a/compiled/.gitkeep b/compiled/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6bc6df8
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,24 @@
+{
+    "name": "tleilax/studip-dockerized",
+    "type": "project",
+    "require": {
+        "symfony/console": "^6.4",
+        "symfony/yaml": "^6.4",
+        "symfony/var-dumper": "^6.4",
+        "symfony/process": "^6.4",
+        "romanpitak/nginx-config-processor": "^0.2.1",
+        "symfony/filesystem": "^6.4"
+    },
+    "license": "gpl",
+    "authors": [
+        {
+            "name": "Jan-Hendrik Willms",
+            "email": "tleilax@gmail.com"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "Studip\\Dockerized\\":"lib/"
+        }
+    }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..203f8ec
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1063 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "f6dc7cf29c327525be62ebd3b59c2b90",
+    "packages": [
+        {
+            "name": "psr/container",
+            "version": "2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "support": {
+                "issues": "https://github.com/php-fig/container/issues",
+                "source": "https://github.com/php-fig/container/tree/2.0.2"
+            },
+            "time": "2021-11-05T16:47:00+00:00"
+        },
+        {
+            "name": "romanpitak/nginx-config-processor",
+            "version": "v0.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/romanpitak/Nginx-Config-Processor.git",
+                "reference": "0dfcbbda696c7baeed8f955e4831696d90e56758"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/romanpitak/Nginx-Config-Processor/zipball/0dfcbbda696c7baeed8f955e4831696d90e56758",
+                "reference": "0dfcbbda696c7baeed8f955e4831696d90e56758",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "codacy/coverage": "dev-master",
+                "codeclimate/php-test-reporter": "dev-master"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "RomanPitak\\Nginx\\Config\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Piták",
+                    "email": "roman@pitak.net",
+                    "homepage": "http://pitak.net",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Nginx configuration files processor.",
+            "homepage": "https://github.com/romanpitak/Nginx-Config-Processor",
+            "keywords": [
+                "conf",
+                "conf file",
+                "config",
+                "config file",
+                "configuration",
+                "configuration file",
+                "create",
+                "creator",
+                "manager",
+                "nginx",
+                "parser",
+                "php",
+                "processor"
+            ],
+            "support": {
+                "email": "roman@pitak.net",
+                "issues": "https://github.com/romanpitak/Nginx-Config-Processor/issues",
+                "source": "https://github.com/romanpitak/Nginx-Config-Processor"
+            },
+            "time": "2016-01-31T19:44:23+00:00"
+        },
+        {
+            "name": "symfony/console",
+            "version": "v6.4.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/console.git",
+                "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/console/zipball/a2708a5da5c87d1d0d52937bdeac625df659e11f",
+                "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "symfony/deprecation-contracts": "^2.5|^3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/service-contracts": "^2.5|^3",
+                "symfony/string": "^5.4|^6.0|^7.0"
+            },
+            "conflict": {
+                "symfony/dependency-injection": "<5.4",
+                "symfony/dotenv": "<5.4",
+                "symfony/event-dispatcher": "<5.4",
+                "symfony/lock": "<5.4",
+                "symfony/process": "<5.4"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0|2.0|3.0"
+            },
+            "require-dev": {
+                "psr/log": "^1|^2|^3",
+                "symfony/config": "^5.4|^6.0|^7.0",
+                "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+                "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+                "symfony/http-foundation": "^6.4|^7.0",
+                "symfony/http-kernel": "^6.4|^7.0",
+                "symfony/lock": "^5.4|^6.0|^7.0",
+                "symfony/messenger": "^5.4|^6.0|^7.0",
+                "symfony/process": "^5.4|^6.0|^7.0",
+                "symfony/stopwatch": "^5.4|^6.0|^7.0",
+                "symfony/var-dumper": "^5.4|^6.0|^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Console\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Eases the creation of beautiful and testable command line interfaces",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "cli",
+                "command-line",
+                "console",
+                "terminal"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/console/tree/v6.4.6"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-03-29T19:07:53+00:00"
+        },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v3.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
+                "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.4-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-05-23T14:45:45+00:00"
+        },
+        {
+            "name": "symfony/filesystem",
+            "version": "v6.4.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/filesystem.git",
+                "reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/9919b5509ada52cc7f66f9a35c86a4a29955c9d3",
+                "reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-mbstring": "~1.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Filesystem\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides basic utilities for the filesystem",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/filesystem/tree/v6.4.6"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-03-21T19:36:20+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.29.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
+                "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-ctype": "*"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-01-29T20:11:03+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-grapheme",
+            "version": "v1.29.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+                "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
+                "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's grapheme_* functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "grapheme",
+                "intl",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-01-29T20:11:03+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-normalizer",
+            "version": "v1.29.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+                "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
+                "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's Normalizer class and related functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "intl",
+                "normalizer",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-01-29T20:11:03+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.29.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+                "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-01-29T20:11:03+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v6.4.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "710e27879e9be3395de2b98da3f52a946039f297"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297",
+                "reference": "710e27879e9be3395de2b98da3f52a946039f297",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Executes commands in sub-processes",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/process/tree/v6.4.4"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-02-20T12:31:00+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v3.4.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "11bbf19a0fb7b36345861e85c5768844c552906e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e",
+                "reference": "11bbf19a0fb7b36345861e85c5768844c552906e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "psr/container": "^1.1|^2.0"
+            },
+            "conflict": {
+                "ext-psr": "<1.1|>=2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.4-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Test/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/service-contracts/tree/v3.4.2"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-12-19T21:51:00+00:00"
+        },
+        {
+            "name": "symfony/string",
+            "version": "v6.4.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/string.git",
+                "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9",
+                "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-intl-grapheme": "~1.0",
+                "symfony/polyfill-intl-normalizer": "~1.0",
+                "symfony/polyfill-mbstring": "~1.0"
+            },
+            "conflict": {
+                "symfony/translation-contracts": "<2.5"
+            },
+            "require-dev": {
+                "symfony/error-handler": "^5.4|^6.0|^7.0",
+                "symfony/http-client": "^5.4|^6.0|^7.0",
+                "symfony/intl": "^6.2|^7.0",
+                "symfony/translation-contracts": "^2.5|^3.0",
+                "symfony/var-exporter": "^5.4|^6.0|^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "Resources/functions.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Component\\String\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "grapheme",
+                "i18n",
+                "string",
+                "unicode",
+                "utf-8",
+                "utf8"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/string/tree/v6.4.4"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-02-01T13:16:41+00:00"
+        },
+        {
+            "name": "symfony/var-dumper",
+            "version": "v6.4.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/var-dumper.git",
+                "reference": "95bd2706a97fb875185b51ecaa6112ec184233d4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/95bd2706a97fb875185b51ecaa6112ec184233d4",
+                "reference": "95bd2706a97fb875185b51ecaa6112ec184233d4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "symfony/deprecation-contracts": "^2.5|^3",
+                "symfony/polyfill-mbstring": "~1.0"
+            },
+            "conflict": {
+                "symfony/console": "<5.4"
+            },
+            "require-dev": {
+                "ext-iconv": "*",
+                "symfony/console": "^5.4|^6.0|^7.0",
+                "symfony/error-handler": "^6.3|^7.0",
+                "symfony/http-kernel": "^5.4|^6.0|^7.0",
+                "symfony/process": "^5.4|^6.0|^7.0",
+                "symfony/uid": "^5.4|^6.0|^7.0",
+                "twig/twig": "^2.13|^3.0.4"
+            },
+            "bin": [
+                "Resources/bin/var-dump-server"
+            ],
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "Resources/functions/dump.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Component\\VarDumper\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides mechanisms for walking through any arbitrary PHP variable",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "debug",
+                "dump"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/var-dumper/tree/v6.4.6"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-03-19T11:56:30+00:00"
+        },
+        {
+            "name": "symfony/yaml",
+            "version": "v6.4.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/yaml.git",
+                "reference": "d75715985f0f94f978e3a8fa42533e10db921b90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/d75715985f0f94f978e3a8fa42533e10db921b90",
+                "reference": "d75715985f0f94f978e3a8fa42533e10db921b90",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "symfony/deprecation-contracts": "^2.5|^3",
+                "symfony/polyfill-ctype": "^1.8"
+            },
+            "conflict": {
+                "symfony/console": "<5.4"
+            },
+            "require-dev": {
+                "symfony/console": "^5.4|^6.0|^7.0"
+            },
+            "bin": [
+                "Resources/bin/yaml-lint"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Yaml\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Loads and dumps YAML files",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/yaml/tree/v6.4.3"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-01-23T14:51:35+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": [],
+    "plugin-api-version": "2.6.0"
+}
diff --git a/php/7.2-Dockerfile b/config/docker-files/7.2-Dockerfile
similarity index 86%
rename from php/7.2-Dockerfile
rename to config/docker-files/7.2-Dockerfile
index 0bb670a..96071ab 100644
--- a/php/7.2-Dockerfile
+++ b/config/docker-files/7.2-Dockerfile
@@ -5,7 +5,7 @@ RUN apt update && apt install -y --no-install-recommends \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -20,14 +20,14 @@ RUN apt update && apt install -y --no-install-recommends \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg-dir --with-freetype-dir --with-webp-dir
-RUN docker-php-ext-install -j$(nproc) gettext curl gd mbstring zip pdo pdo_mysql pdo_pgsql mysqli intl json soap
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg-dir --with-freetype-dir --with-webp-dir
+RUN docker-php-ext-install -j$(nproc) gd gettext intl mysqli pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached & redis
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/php/7.3-Dockerfile b/config/docker-files/7.3-Dockerfile
similarity index 86%
rename from php/7.3-Dockerfile
rename to config/docker-files/7.3-Dockerfile
index c6e13ff..3c1ff34 100644
--- a/php/7.3-Dockerfile
+++ b/config/docker-files/7.3-Dockerfile
@@ -5,7 +5,7 @@ RUN apt update && apt install -y --no-install-recommends \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -20,14 +20,14 @@ RUN apt update && apt install -y --no-install-recommends \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg-dir --with-freetype-dir --with-webp-dir
-RUN docker-php-ext-install -j$(nproc) gettext curl gd mbstring zip pdo pdo_mysql pdo_pgsql mysqli intl json soap
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg-dir --with-freetype-dir --with-webp-dir
+RUN docker-php-ext-install -j$(nproc) gd gettext intl mysqli pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/php/7.4-Dockerfile b/config/docker-files/7.4-Dockerfile
similarity index 86%
rename from php/7.4-Dockerfile
rename to config/docker-files/7.4-Dockerfile
index 4a74f89..fb3afe0 100644
--- a/php/7.4-Dockerfile
+++ b/config/docker-files/7.4-Dockerfile
@@ -5,7 +5,7 @@ RUN apt update && apt install -y --no-install-recommends \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -20,14 +20,14 @@ RUN apt update && apt install -y --no-install-recommends \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
-RUN docker-php-ext-install -j$(nproc) gettext curl gd mbstring zip pdo pdo_mysql pdo_pgsql mysqli intl json soap
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
+RUN docker-php-ext-install -j$(nproc) gd gettext intl pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached & redis
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/php/8.0-Dockerfile b/config/docker-files/8.0-Dockerfile
similarity index 86%
rename from php/8.0-Dockerfile
rename to config/docker-files/8.0-Dockerfile
index 2f795f2..9fd1fc6 100644
--- a/php/8.0-Dockerfile
+++ b/config/docker-files/8.0-Dockerfile
@@ -5,7 +5,7 @@ RUN apt update && apt install -y --no-install-recommends \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -20,14 +20,14 @@ RUN apt update && apt install -y --no-install-recommends \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
-RUN docker-php-ext-install -j$(nproc) pdo gettext curl gd mbstring zip pdo_mysql pdo_pgsql mysqli intl soap
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
+RUN docker-php-ext-install -j$(nproc) gd gettext intl mysqli pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached & redis
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/php/8.1-Dockerfile b/config/docker-files/8.1-Dockerfile
similarity index 86%
rename from php/8.1-Dockerfile
rename to config/docker-files/8.1-Dockerfile
index 3bd5c8e..b6eb7da 100644
--- a/php/8.1-Dockerfile
+++ b/config/docker-files/8.1-Dockerfile
@@ -5,7 +5,7 @@ RUN apt update && apt install -y --no-install-recommends \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -20,14 +20,14 @@ RUN apt update && apt install -y --no-install-recommends \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
-RUN docker-php-ext-install -j$(nproc) pdo gettext curl gd mbstring zip pdo_mysql pdo_pgsql mysqli intl soap
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
+RUN docker-php-ext-install -j$(nproc) gd gettext intl mysqli pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached & redis
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/php/8.2-Dockerfile b/config/docker-files/8.2-Dockerfile
similarity index 86%
rename from php/8.2-Dockerfile
rename to config/docker-files/8.2-Dockerfile
index 16639e4..69b9d25 100644
--- a/php/8.2-Dockerfile
+++ b/config/docker-files/8.2-Dockerfile
@@ -5,7 +5,7 @@ RUN apt update && apt install -y --no-install-recommends \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -20,14 +20,14 @@ RUN apt update && apt install -y --no-install-recommends \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
-RUN docker-php-ext-install -j$(nproc) pdo gettext curl gd mbstring zip pdo_mysql pdo_pgsql mysqli intl soap
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
+RUN docker-php-ext-install -j$(nproc) gd gettext intl mysqli pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached & redis
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/php/8.3-Dockerfile b/config/docker-files/8.3-Dockerfile
similarity index 87%
rename from php/8.3-Dockerfile
rename to config/docker-files/8.3-Dockerfile
index 5604235..7f8df20 100644
--- a/php/8.3-Dockerfile
+++ b/config/docker-files/8.3-Dockerfile
@@ -6,7 +6,7 @@ RUN apt update \
         default-mysql-client \
         default-libmysqlclient-dev \
         imagemagick ghostscript \
-        libcurl4-openssl-dev zlib1g-dev \
+        zlib1g-dev \
         libjpeg-dev \
         libpng-dev \
         libpq-dev \
@@ -21,14 +21,14 @@ RUN apt update \
         vim \
     && rm -rf /var/lib/apt/lists/*
 
-# Install php extensions
-RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
-RUN docker-php-ext-install -j$(nproc) curl gd gettext intl mbstring mysqli pdo pdo_mysql pdo_pgsql soap zip
-
 # Install de_DE locale
 RUN locale-gen de_DE.UTF-8 \
     && update-locale
 
+# Install php extensions
+RUN docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp
+RUN docker-php-ext-install -j$(nproc) gd gettext intl mysqli pdo_mysql pdo_pgsql soap zip
+
 # Install Memcached and redis
 RUN pecl install memcached redis \
     && docker-php-ext-enable memcached redis
diff --git a/config/nginx-php.conf b/config/nginx-php.conf
deleted file mode 100644
index f1329d4..0000000
--- a/config/nginx-php.conf
+++ /dev/null
@@ -1,11 +0,0 @@
-location ~ \.php(?:$|/) {
-    include fastcgi_params;
-    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
-    fastcgi_intercept_errors on;
-    fastcgi_pass $fastcgi_backend;
-    fastcgi_index index.php;
-    fastcgi_param SERVER_NAME $http_host;
-    fastcgi_param DOCUMENT_ROOT $site_document_root;
-    fastcgi_param PATH_INFO $fastcgi_path_info;
-    fastcgi_param SCRIPT_FILENAME $request_filename;
-}
diff --git a/config/nginx-sites.conf b/config/nginx-sites.conf
deleted file mode 100644
index f6784aa..0000000
--- a/config/nginx-sites.conf
+++ /dev/null
@@ -1,78 +0,0 @@
-location / {
-    include nginx-php.conf;
-}
-
-location ^~ /trunk {
-    alias /var/www/html/studip/trunk/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/trunk/public;
-    include nginx-php.conf;
-}
-
-location ^~ /5.0 {
-    alias /var/www/html/studip/5.0/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/5.0/public;
-    include nginx-php.conf;
-}
-location ^~ /5.1 {
-    alias /var/www/html/studip/5.1/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/5.1/public;
-    include nginx-php.conf;
-}
-location ^~ /5.2 {
-    alias /var/www/html/studip/5.2/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/5.2/public;
-    include nginx-php.conf;
-}
-location ^~ /5.3 {
-    alias /var/www/html/studip/5.3/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/5.3/public;
-    include nginx-php.conf;
-}
-location ^~ /5.4 {
-    alias /var/www/html/studip/5.4/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/5.4/public;
-    include nginx-php.conf;
-}
-location ^~ /5.5 {
-    alias /var/www/html/studip/5.5/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/5.5/public;
-    include nginx-php.conf;
-}
-
-location ^~ /uol-5.4 {
-    alias /var/www/html/studip/uol/5.4/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/uol/5.4/public;
-    include nginx-php.conf;
-}
-
-location ^~ /uol-5.1 {
-    alias /var/www/html/studip/uol/5.1/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/uol/5.1/public;
-    include nginx-php.conf;
-}
-
-location ^~ /peoe-4.6 {
-    alias /var/www/html/studip/uol/peoe-4.6/public;
-    index index.php index.html index.html;
-
-    set $site_document_root /var/www/html/studip/uol/peoe-4.6/public;
-    include nginx-php.conf;
-}
diff --git a/config/nginx.conf b/config/nginx.conf
deleted file mode 100644
index d91e17a..0000000
--- a/config/nginx.conf
+++ /dev/null
@@ -1,91 +0,0 @@
-upstream php74_backend {
-   server php74:9000;
-}
-upstream php80_backend {
-   server php80:9000;
-}
-upstream php81_backend {
-   server php81:9000;
-}
-upstream php82_backend {
-   server php82:9000;
-}
-upstream php83_backend {
-   server php83:9000;
-}
-upstream php72_backend {
-   server php72:9000;
-}
-upstream php73_backend {
-   server php73:9000;
-}
-
-server {
-    listen 80 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php74_backend;
-
-    include sites.conf;
-}
-
-server {
-    listen 81 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php80_backend;
-
-    include sites.conf;
-}
-
-server {
-    listen 82 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php81_backend;
-
-    include sites.conf;
-}
-
-server {
-    listen 83 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php82_backend;
-
-    include sites.conf;
-}
-
-server {
-    listen 84 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php83_backend;
-
-    include sites.conf;
-}
-
-server {
-    listen 88 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php72_backend;
-
-    include sites.conf;
-}
-
-server {
-    listen 89 default_server;
-    server_name _;
-    root /var/www/html;
-
-    set $fastcgi_backend php73_backend;
-
-    include sites.conf;
-}
diff --git a/data/index.php b/data/index.php
deleted file mode 100644
index 61ace19..0000000
--- a/data/index.php
+++ /dev/null
@@ -1,2 +0,0 @@
-<?php
-phpinfo();
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index f53ac7a..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,75 +0,0 @@
-version: "3"
-
-services:
-  nginx:
-    image: nginx:alpine
-    ports:
-      - "8074:80"
-      - "8080:81"
-      - "8081:82"
-      - "8082:83"
-      - "8083:84"
-      - "8072:88"
-      - "8073:89"
-    networks:
-      - code-network
-    depends_on:
-      - redis-server
-      - memcached-server
-    volumes:
-      - ./config/nginx.conf:/etc/nginx/conf.d/default.conf
-      - ./config/nginx-sites.conf:/etc/nginx/sites.conf
-      - ./config/nginx-php.conf:/etc/nginx/nginx-php.conf
-      - ./data:/var/www/html
-      - ~/Code/studip:/var/www/html/studip:ro
-      - ./logs/nginx:/var/log/nginx/
-  redis-server:
-    image: redis:latest
-    networks:
-      - code-network
-  memcached-server:
-    image: memcached:latest
-    networks:
-      - code-network
-  php74: &base-php
-    build:
-      context: .
-      dockerfile: php/7.4-Dockerfile
-    environment:
-      - MYSQL_HOST=host.docker.internal
-    networks:
-      - code-network
-    volumes:
-      - ./config/php-fpm.conf:/usr/local/etc/php-fpm.d/zz-config.conf
-      - ./data:/var/www/html
-      - ~/Code/studip:/var/www/html/studip
-      - ./logs/php/access.log:/var/log/php-access.log
-      - ./logs/php/error.log:/var/log/php-error.log
-  php80:
-    <<: *base-php
-    build:
-      dockerfile: php/8.0-Dockerfile
-  php81:
-    <<: *base-php
-    build:
-      dockerfile: php/8.1-Dockerfile
-  php82:
-    <<: *base-php
-    build:
-      dockerfile: php/8.2-Dockerfile
-  php83:
-    <<: *base-php
-    build:
-      dockerfile: php/8.3-Dockerfile
-  php72:
-    <<: *base-php
-    build:
-      dockerfile: php/7.2-Dockerfile
-  php73:
-    <<: *base-php
-    build:
-      dockerfile: php/7.3-Dockerfile
-
-networks:
-  code-network:
-    driver: bridge
diff --git a/generate.sh b/generate.sh
deleted file mode 100755
index 20da5cb..0000000
--- a/generate.sh
+++ /dev/null
@@ -1 +0,0 @@
-docker compose up --build
diff --git a/lib/Commands/Compile.php b/lib/Commands/Compile.php
new file mode 100644
index 0000000..3ead649
--- /dev/null
+++ b/lib/Commands/Compile.php
@@ -0,0 +1,233 @@
+<?php
+namespace Studip\Dockerized\Commands;
+
+use Studip\Dockerized\Config;
+use Studip\Dockerized\Creators\DockerComposeConfiguration;
+use Studip\Dockerized\Creators\NginxConfiguration;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+final class Compile extends \Symfony\Component\Console\Command\Command
+{
+    protected function configure()
+    {
+        $this->setName('compile');
+        $this->setDescription('Compiles the configuration files');
+        $this->addOption(
+            'path', '
+            p',
+            InputOption::VALUE_OPTIONAL,
+            'Path to store the compiled files',
+            realpath(__DIR__ . '/../../compiled')
+        );
+        $this->addOption('nginx', null, InputOption::VALUE_NEGATABLE, 'Compile nginx configuration', true);
+        $this->addOption('docker', null, InputOption::VALUE_NEGATABLE, 'Compile docker configuration', true);
+    }
+
+    protected function initialize(InputInterface $input, OutputInterface $output)
+    {
+        if (!file_exists($input->getOption('path')) && !mkdir($input->getOption('path'))) {
+            $output->writeln('<error>Could not create compile directory</error>');
+            return Command::FAILURE;
+        }
+
+        if (!is_dir($input->getOption('path'))) {
+            $output->writeln('<error>Given compile directory is invalid (not a directory)</error>');
+            return Command::FAILURE;
+        }
+
+        if (!is_writable($input->getOption('path'))) {
+            $output->writeln('<error>Given compile directory is invalid (not writable)</error>');
+            return Command::FAILURE;
+        }
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $compiledPath = realpath($input->getOption('path'));
+        $io = new SymfonyStyle($input, $output);
+
+        $writeConfigFile = function (string $filename, string $content) use ($compiledPath, $io): void {
+            file_put_contents(
+                "{$compiledPath}/{$filename}",
+                $content
+            );
+            $io->success('Config file ' . $filename . ' written');
+        };
+
+        if ($input->getOption('nginx')) {
+            $writeConfigFile(
+                'nginx.conf',
+                $this->createNginxConfiguration()
+            );
+        }
+
+        if ($input->getOption('docker')) {
+            $writeConfigFile(
+                'docker-compose.yml',
+                $this->createDockerComposerConfiguration(
+                    realpath(__DIR__ . '/../..'),
+                    $compiledPath
+                )
+            );
+        }
+
+        return Command::SUCCESS;
+    }
+
+    private function getPHPVersionIndex(string $version, string $prefix = ''): string
+    {
+        return $prefix . preg_replace('/\D/', '', $version);
+    }
+
+    public function createDockerComposerConfiguration(string $cwd, string $compiledPath): string
+    {
+        $creator = new DockerComposeConfiguration('studip-dockerized');
+
+        // Declare volumes
+        $volumes = [
+            [realpath(__DIR__ . '/../../web'), '/var/www/html/studip-dockerized-config.app', 'ro'],
+            [realpath(__DIR__ . '/../../config.json'), '/var/www/html/studip-dockerized-config.json', 'ro'],
+            ...array_map(
+                fn(string $path, array $definition) => [$definition['source'], '/var/www/html/' . $path],
+                array_keys(Config::getInstance()->get('sites') ?? []),
+                array_values(Config::getInstance()->get('sites') ?? [])
+            )
+        ];
+
+        // Nginx
+        $creator->addService('nginx', 'nginx:alpine', [
+            'networks' => ['code-network'],
+            'depends_on' => ['redis-server', 'memcached-server'],
+            'ports' => array_map(
+                function ($port) {
+                    return "{$port}:{$port}";
+                },
+                array_values(Config::getInstance()->get('php'))
+            ),
+        ]);
+
+        $creator->addServiceVolume(
+            'nginx',
+            "{$compiledPath}/nginx.conf",
+            '/etc/nginx/conf.d/default.conf',
+            'ro'
+        );
+        $creator->addServiceVolume('nginx', realpath("{$cwd}/logs/nginx"), '/var/log/nginx');
+
+
+        foreach ($volumes as $volume) {
+            $creator->addServiceVolume('nginx', ...$volume);
+        }
+
+        // PHP
+        foreach (Config::getInstance()->get('php') as $version => $port) {
+            $index = $this->getPHPVersionIndex($version, 'php');
+
+            $creator->addService($index, null, [
+                'build' => [
+                    'context' => '.',
+                    'dockerfile' => "{$cwd}/config/docker-files/{$version}-Dockerfile",
+                ],
+                'environment' => [
+                    'MYSQL_HOST' => 'host.docker.internal',
+                ],
+                'networks' => ['code-network'],
+            ]);
+
+            $creator->addServiceVolume(
+                $index,
+                "{$cwd}/config/php-fpm.conf",
+                '/usr/local/etc/php-fpm.d/zz-config.conf',
+                'ro'
+            );
+            $creator->addServiceVolume(
+                $index,
+                "{$cwd}/logs/php",
+                '/var/log/'
+            );
+
+            foreach ($volumes as $volume) {
+                $creator->addServiceVolume($index, ...$volume);
+            }
+        }
+
+        // Other needed images
+        $creator->addService('memcached-server', 'memcached:latest', [
+            'networks' => ['code-network'],
+        ]);
+
+        $creator->addService('redis-server', 'redis:latest', [
+            'networks' => ['code-network'],
+        ]);
+
+        $creator->addNetwork('code-network', [
+            'driver' => 'bridge',
+        ]);
+
+
+        return $creator->dump();
+    }
+
+    public function createNginxConfiguration(): string
+    {
+        $creator = new NginxConfiguration();
+
+        // Register event handler to add the nginx-php configuration to each location
+        $creator->on(NginxConfiguration::EVENT_LOCATION_ADD_BEFORE, function ($location, &$config) use ($creator) {
+            $config[] = $creator->write('location ~ \.php(?:$|/)', [
+                'include fastcgi_params',
+                'fastcgi_split_path_info ^(.+?\.php)(/.*)$',
+                'fastcgi_intercept_errors on',
+                'fastcgi_pass $fastcgi_backend',
+                'fastcgi_index index.php',
+                'fastcgi_param SERVER_NAME $http_host',
+                'fastcgi_param DOCUMENT_ROOT $site_document_root',
+                'fastcgi_param PATH_INFO $fastcgi_path_info',
+                'fastcgi_param SCRIPT_FILENAME $request_filename',
+            ]);
+        });
+
+        // Add default location
+        $creator->addLocation('/sites', [
+            'alias /var/www/html/studip-dockerized-config.app',
+            'index index.php index.html index.html',
+            'set $site_document_root /var/www/html/studip-dockerized-config.app',
+        ]);
+
+        // Add configured locations
+        foreach (Config::getInstance()->get('sites') ?? [] as $path => $definition) {
+            $p = '/var/www/html/' . $path;
+            if (isset($definition['mount'])) {
+                $p .= '/' . $definition['mount'];
+            }
+
+            $creator->addLocation($path, [
+                'alias ' . $p,
+                'index index.php index.html index.html',
+                'set $site_document_root ' . $p,
+            ]);
+        }
+
+        // Add upstreams and servers
+        foreach (Config::getInstance()->get('php') as $version => $port) {
+            $index = $this->getPHPVersionIndex($version, 'php');
+
+            $creator->addUpstream("{$index}_backend", [
+                "server {$index}:9000"
+            ]);
+
+            $creator->addServer([
+                'listen ' . $port . ' default_server',
+                'server_name _',
+                'root /var/www/html',
+                'set $fastcgi_backend ' . $index . '_backend',
+            ]);
+        }
+
+        return $creator->dump();
+    }
+}
diff --git a/lib/Commands/Docker/Build.php b/lib/Commands/Docker/Build.php
new file mode 100644
index 0000000..080e444
--- /dev/null
+++ b/lib/Commands/Docker/Build.php
@@ -0,0 +1,17 @@
+<?php
+namespace Studip\Dockerized\Commands\Docker;
+
+use Studip\Dockerized\DockerComposeCommand;
+
+final class Build extends DockerComposeCommand
+{
+    protected function configure()
+    {
+        $this->setName('docker:build');
+        $this->setDescription('Build containers');
+    }
+    protected function getDockerComposeCommand(): array
+    {
+        return ['up', '--no-start'];
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/Docker/Start.php b/lib/Commands/Docker/Start.php
new file mode 100644
index 0000000..601769e
--- /dev/null
+++ b/lib/Commands/Docker/Start.php
@@ -0,0 +1,18 @@
+<?php
+namespace Studip\Dockerized\Commands\Docker;
+
+use Studip\Dockerized\DockerComposeCommand;
+
+final class Start extends DockerComposeCommand
+{
+    protected function configure()
+    {
+        $this->setName('docker:start');
+        $this->setDescription('Starts docker');
+    }
+
+    protected function getDockerComposeCommand(): array
+    {
+        return ['start'];
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/Docker/Stop.php b/lib/Commands/Docker/Stop.php
new file mode 100644
index 0000000..e81d410
--- /dev/null
+++ b/lib/Commands/Docker/Stop.php
@@ -0,0 +1,18 @@
+<?php
+namespace Studip\Dockerized\Commands\Docker;
+
+use Studip\Dockerized\DockerComposeCommand;
+
+final class Stop extends DockerComposeCommand
+{
+    protected function configure()
+    {
+        $this->setName('docker:stop');
+        $this->setDescription('Stops docker');
+    }
+
+    protected function getDockerComposeCommand(): array
+    {
+        return ['stop'];
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/Init.php b/lib/Commands/Init.php
new file mode 100644
index 0000000..c192510
--- /dev/null
+++ b/lib/Commands/Init.php
@@ -0,0 +1,45 @@
+<?php
+namespace Studip\Dockerized\Commands;
+
+use Studip\Dockerized\Config;
+use Studip\Dockerized\SupportedPHPVersions;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+final class Init extends Command
+{
+    protected function configure()
+    {
+        $this->setName('init');
+        $this->setDescription('Inits studip-dockerized');
+
+        $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force creation of config file');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        if (file_exists(CONFIG_FILE) && !$input->getOption('force')) {
+            $io->error('Config file already exists.');
+            return Command::FAILURE;
+        }
+
+        $config = Config::create(CONFIG_FILE, false);
+        $config->set('php', array_combine(
+            SupportedPHPVersions::getDefaultVersions(),
+            array_map(
+                fn(string $version): int => SupportedPHPVersions::mapPort($version),
+                SupportedPHPVersions::getDefaultVersions()
+            )
+        ));
+        $config->store();
+
+        $io->success('Config file has been created');
+
+        return Command::SUCCESS;
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/PHP/Disable.php b/lib/Commands/PHP/Disable.php
new file mode 100644
index 0000000..cf91e12
--- /dev/null
+++ b/lib/Commands/PHP/Disable.php
@@ -0,0 +1,55 @@
+<?php
+namespace Studip\Dockerized\Commands\PHP;
+
+use Studip\Dockerized\Config;
+use Studip\Dockerized\SupportedPHPVersions;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+final class Disable extends Command
+{
+    protected function configure()
+    {
+        $this->setName('php:disable');
+        $this->setDescription('Disable PHP version (needs recompile)');
+        $this->addArgument(
+            'version',
+            InputArgument::REQUIRED | InputArgument::IS_ARRAY,
+            'PHP version(s) that should be disabled',
+            null,
+            SupportedPHPVersions::getVersions()
+        );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $config = Config::getInstance();
+        $io = new SymfonyStyle($input, $output);
+
+        $disabled = 0;
+        foreach ($input->getArgument('version') as $version) {
+            if (!isset($config->get('php')[$version])) {
+                $io->error("PHP version {$version} is not enabled");
+                continue;
+            }
+
+            $php = $config->get('php');
+            unset($php[$version]);
+            $config->set('php', $php);
+
+            $io->success("PHP version {$version} has been disabled");
+            $disabled += 1;
+        }
+
+        if ($disabled === 0) {
+            return Command::FAILURE;
+        }
+
+        $config->store();
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/lib/Commands/PHP/Enable.php b/lib/Commands/PHP/Enable.php
new file mode 100644
index 0000000..67eac9b
--- /dev/null
+++ b/lib/Commands/PHP/Enable.php
@@ -0,0 +1,57 @@
+<?php
+namespace Studip\Dockerized\Commands\PHP;
+
+use Studip\Dockerized\Config;
+use Studip\Dockerized\SupportedPHPVersions;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+final class Enable extends Command
+{
+    protected function configure()
+    {
+        $this->setName('php:enable');
+        $this->setDescription('Enable PHP version (needs recompile)');
+        $this->addArgument(
+            'version',
+            InputArgument::REQUIRED | InputArgument::IS_ARRAY,
+            'PHP version(s) that should be enabled',
+            null,
+            SupportedPHPVersions::getVersions()
+        );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $config = Config::getInstance();
+        $io = new SymfonyStyle($input, $output);
+
+        $enabled = 0;
+        foreach ($input->getArgument('version') as $version) {
+            if (isset($config->get('php')[$version])) {
+                $io->error("PHP version {$version} is already enabled");
+                continue;
+            }
+
+            $port = SupportedPHPVersions::mapPort($version);
+
+            $php = $config->get('php');
+            $php[$version] = $port;
+            $config->set('php', $php);
+
+            $io->success("PHP version {$version} has been enabled on port {$port}");
+            $enabled += 1;
+        }
+
+        if ($enabled === 0) {
+            return Command::FAILURE;
+        }
+
+        $config->store();
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/lib/Commands/PHP/Port.php b/lib/Commands/PHP/Port.php
new file mode 100644
index 0000000..18cdf70
--- /dev/null
+++ b/lib/Commands/PHP/Port.php
@@ -0,0 +1,53 @@
+<?php
+namespace Studip\Dockerized\Commands\PHP;
+
+use Studip\Dockerized\Config;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+final class Port extends Command
+{
+    protected function configure()
+    {
+        $this->setName('php:port');
+        $this->setDescription('Change port for PHP version (needs recompile)');
+        $this->addArgument(
+            'version',
+            InputArgument::REQUIRED,
+            'PHP version',
+            null,
+            array_keys(Config::getInstance()->get('php'))
+        );
+        $this->addArgument(
+            'port',
+            InputArgument::REQUIRED,
+            'Port to use'
+        );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $version = $input->getArgument('version');
+        $port = $input->getArgument('port');
+
+        $config = Config::getInstance();
+        $io = new SymfonyStyle($input, $output);
+
+        if (!isset($config->get('php')[$version])) {
+            $io->error("PHP version {$version} is not enabled");
+            return Command::FAILURE;
+        }
+
+        $php = $config->get('php');
+        $php[$version] = (int) $port;
+        $config->set('php', $php);
+        $config->store();
+
+        $io->success("Port of PHP version {$version} was changed to {$port}");
+
+        return Command::SUCCESS;
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/Sites/Add.php b/lib/Commands/Sites/Add.php
new file mode 100644
index 0000000..0bbd870
--- /dev/null
+++ b/lib/Commands/Sites/Add.php
@@ -0,0 +1,122 @@
+<?php
+namespace Studip\Dockerized\Commands\Sites;
+
+use Exception;
+use Studip\Dockerized\Config;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\Question;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Filesystem\Path;
+
+final class Add extends Command
+{
+    protected function configure()
+    {
+        $this->setName('site:add');
+        $this->setDescription('Adds a site (needs recompile)');
+        $this->addArgument('webpath', InputArgument::OPTIONAL, 'The web path', '');
+        $this->addArgument('localpath', InputArgument::OPTIONAL, 'The local path', '');
+        $this->addArgument('mount', InputArgument::OPTIONAL, 'The mounted folder', null);
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        $question = $this->getWebPathQuestion();
+        $webPath = $input->hasArgument('webpath')
+            ? $question->getValidator()($input->getArgument('webpath'))
+            : $io->askQuestion($question);
+
+        $question = $this->getLocalPathQuestion();
+        $localPath = $input->hasArgument('webpath')
+            ? $question->getValidator()($input->getArgument('localpath'))
+            : $io->askQuestion($question);
+
+        $mount = $input->hasArgument('webpath')
+            ? $input->getArgument('mount')
+            : $io->ask('Mounted folder:');
+
+        $this->addSiteToConfig($webPath, $localPath, $mount);
+
+        $io->success('The site "' . $webPath . '" was added.');
+
+        return Command::SUCCESS;
+    }
+
+    private function getWebPathQuestion(): Question
+    {
+        $question = new Question('Web path');
+        $question->setValidator(function (string $webPath): string {
+            $webPath = trim($webPath);
+
+            if (!$webPath) {
+                throw new Exception('Web path cannot be empty');
+            }
+
+            if (array_key_exists($webPath, Config::getInstance()->get('sites') ?? [])) {
+                throw new Exception('Site "' . $webPath . '" already exists.');
+            }
+
+            return $webPath;
+        });
+        return $question;
+    }
+
+    private function getLocalPathQuestion(): Question
+    {
+        $question = new Question('Local path');
+        $question->setValidator(function (string $localPath): string {
+            $localPath = trim($localPath);
+            $localPath = Path::canonicalize($localPath);
+
+            if (!file_exists($localPath)) {
+                throw new Exception('Local path does not exist');
+            }
+
+            if (!is_readable($localPath)) {
+                throw new Exception('Local path is not readable.');
+            }
+
+            return $localPath;
+
+        });
+        $question->setAutocompleterCallback(function (string $input): array {
+            // Strip any characters from the last slash to the end of the string
+            // to keep only the last directory and generate suggestions for it
+            $inputPath = preg_replace('%(/|^)[^/]*$%', '$1', $input);
+            $inputPath = '' === $inputPath ? getcwd() : $inputPath;
+
+            if (
+                !file_exists(Path::canonicalize($inputPath))
+                || !is_readable(Path::canonicalize($inputPath))
+            ) {
+                return [];
+            }
+
+            // CAUTION - this example code allows unrestricted access to the
+            // entire filesystem. In real applications, restrict the directories
+            // where files and dirs can be found
+            $foundFilesAndDirs = @scandir(Path::canonicalize($inputPath)) ?: [];
+
+            return array_map(function (string $dirOrFile) use ($inputPath): string {
+                return $inputPath . $dirOrFile;
+            }, $foundFilesAndDirs);
+        });
+        return $question;
+    }
+
+    public function addSiteToConfig(mixed $webPath, string $source, ?string $mount): void
+    {
+        $config = Config::getInstance();
+        $sites = $config->get('sites') ?? [];
+        $sites[$webPath] = array_filter(compact('source', 'mount'));
+        ksort($sites);
+        $config->set('sites', $sites);
+
+        $config->store();
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/Sites/Remove.php b/lib/Commands/Sites/Remove.php
new file mode 100644
index 0000000..c435b62
--- /dev/null
+++ b/lib/Commands/Sites/Remove.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Studip\Dockerized\Commands\Sites;
+
+use Studip\Dockerized\Config;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+final class Remove extends Command
+{
+    protected function configure()
+    {
+        $this->setName('site:remove');
+        $this->setDescription('Remove a site (needs recompile)');
+        $this->addArgument('webpath', InputArgument::REQUIRED, 'The web path');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        $webPath = $input->getArgument('webpath');
+
+        $config = Config::getInstance();
+        $sites = $config->get('sites') ?? [];
+
+        if (!array_key_exists($webPath, $sites)) {
+            $io->error("The site '{$webPath}' does not exist.");
+            return Command::FAILURE;
+        }
+
+        unset($sites[$webPath]);
+        if (count($sites) > 0) {
+            ksort($sites);
+            $config->set('sites', $sites);
+        } else {
+            $config->delete('sites');
+        }
+
+        $config->store();
+
+        $io->success('Site ' . $webPath . ' has been removed.');
+
+        return Command::SUCCESS;
+    }
+}
\ No newline at end of file
diff --git a/lib/Commands/Sites/Show.php b/lib/Commands/Sites/Show.php
new file mode 100644
index 0000000..ab081e8
--- /dev/null
+++ b/lib/Commands/Sites/Show.php
@@ -0,0 +1,33 @@
+<?php
+namespace Studip\Dockerized\Commands\Sites;
+
+use Studip\Dockerized\Config;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+final class Show extends Command
+{
+    protected function configure()
+    {
+        $this->setName('site:list');
+        $this->setDescription('List all sites');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $table = new Table($output);
+        //$table->setStyle('compact');
+
+        $table->setHeaders(['Web', 'Local', 'Mount']);
+
+        foreach (Config::getInstance()->get('sites') ?? [] as $key => $site) {
+            $table->addRow([$key, $site['source'], $site['mount'] ?? '-']);
+        };
+
+        $table->render();
+
+        return Command::SUCCESS;
+    }
+}
\ No newline at end of file
diff --git a/lib/Config.php b/lib/Config.php
new file mode 100644
index 0000000..147ed44
--- /dev/null
+++ b/lib/Config.php
@@ -0,0 +1,84 @@
+<?php
+namespace Studip\Dockerized;
+
+final class Config
+{
+    private static ?Config $instance = null;
+
+    public static function load(string $filename): void
+    {
+        self::$instance = self::create($filename);
+    }
+
+    public static function create(string $filename, bool $initialReset = true): Config
+    {
+        return new self($filename, $initialReset);
+    }
+
+    public static function getInstance(): Config
+    {
+        if (!isset(self::$instance)) {
+            throw new \Exception('Config has not been loaded');
+        }
+        return self::$instance;
+    }
+
+    private array $data = [];
+    private string $filename;
+
+    private function __construct(string $filename, bool $initialReset = true)
+    {
+        $this->filename = $filename;
+
+        if ($initialReset) {
+            $this->reset();
+        }
+    }
+
+    public function get(string $key): mixed
+    {
+        return $this->data[$key] ?? null;
+    }
+
+    public function set(string $key, mixed $value): void
+    {
+        $this->data[$key] = $value;
+    }
+
+    public function has(string $key): bool
+    {
+        return array_key_exists($key, $this->data);
+    }
+
+    public function delete(string $key): void
+    {
+        unset($this->data[$key]);
+    }
+
+    public function dump(): string
+    {
+        return json_encode($this->data, JSON_PRETTY_PRINT);
+    }
+
+    public function reset(): void
+    {
+        if (!file_exists($this->filename)) {
+            throw new \Exception("Config file '{$this->filename}' does not exist");
+        }
+
+        if (!is_readable($this->filename)) {
+            throw new \Exception("Config file '{$this->filename}' is not readable");
+        }
+
+        $this->data = json_decode(file_get_contents($this->filename), true);
+    }
+
+    public function store(): void
+    {
+        if (!is_writable($this->filename)) {
+            throw new \Exception("Config file '{$this->filename}' is not writable");
+        }
+
+        file_put_contents($this->filename, $this->dump());
+    }
+}
\ No newline at end of file
diff --git a/lib/Creators/DockerComposeConfiguration.php b/lib/Creators/DockerComposeConfiguration.php
new file mode 100644
index 0000000..0c494f3
--- /dev/null
+++ b/lib/Creators/DockerComposeConfiguration.php
@@ -0,0 +1,70 @@
+<?php
+namespace Studip\Dockerized\Creators;
+
+use Symfony\Component\Yaml\Yaml;
+
+final class DockerComposeConfiguration
+{
+    private array $configuration = [];
+    private string $cwd;
+
+    public function __construct(string $cwd, string $name = '')
+    {
+        $this->cwd = $cwd;
+
+        if ($name) {
+            $this->configuration[$name] = $name;
+        }
+    }
+
+    public function addService(string $service, ?string $image, array $config = []): void
+    {
+        if (!isset($this->configuration['services'])) {
+            $this->configuration['services'] = [];
+        }
+        $this->configuration['services'][$service] = array_merge(
+            isset($image) ? compact('image') : [],
+            $config
+        );
+    }
+
+    public function extendService(string $service, array $config): void
+    {
+        if (!isset($this->configuration['services'][$service])) {
+            throw new \Exception('Service "' . $service . '" does not exist');
+        }
+
+
+    }
+
+    public function addServiceVolume(string $service, string $source, string $target, string $modifier = ''): void
+    {
+        if (!isset($this->configuration['services'][$service])) {
+            throw new \Exception('Service "' . $service . '" does not exist');
+        }
+
+        if (!isset($this->configuration['services'][$service]['volumes'])) {
+            $this->configuration['services'][$service]['volumes'] = [];
+        }
+
+        $this->configuration['services'][$service]['volumes'][] = implode(':', array_filter([
+            $source,
+            $target,
+            $modifier,
+        ]));
+    }
+
+    public function addNetwork(string $network, array $config): void
+    {
+        if (!isset($this->configuration['networks'])) {
+            $this->configuration['networks'] = [];
+        }
+        $this->configuration['networks'][$network] = $config;
+    }
+
+
+    public function dump(): string
+    {
+        return Yaml::dump($this->configuration, 128);
+    }
+}
\ No newline at end of file
diff --git a/lib/Creators/NginxConfiguration.php b/lib/Creators/NginxConfiguration.php
new file mode 100644
index 0000000..fba16ee
--- /dev/null
+++ b/lib/Creators/NginxConfiguration.php
@@ -0,0 +1,109 @@
+<?php
+namespace Studip\Dockerized\Creators;
+
+use RomanPitak\Nginx\Config\Scope;
+
+final class NginxConfiguration
+{
+    public const EVENT_LOCATION_ADD_BEFORE = 'location:add:before';
+    public const EVENT_SERVER_ADD_BEFORE = 'server:add:before';
+    public const EVENT_UPSTREAM_ADD_BEFORE = 'upstream:add:before';
+
+    private array $locations = [];
+    private array $servers = [];
+    private array $upstreams = [];
+
+    private array $event_handlers = [];
+
+    public function __construct()
+    {
+    }
+
+    public function on(string $event, \Closure $handler): void
+    {
+        if (!isset($this->event_handlers[$event])) {
+            $this->event_handlers[$event] = [];
+        }
+        $this->event_handlers[$event][] = $handler;
+    }
+
+    private function trigger(string $event, &...$data): void
+    {
+        if (isset($this->event_handlers[$event])) {
+            foreach ($this->event_handlers[$event] as $handler) {
+                $handler(...$data);
+            }
+        }
+    }
+
+    public function addLocation(string $location, array $config, string $modifier = '^~')
+    {
+        $location = '/' . ltrim($location, '/');
+
+        $this->trigger(self::EVENT_LOCATION_ADD_BEFORE, $location, $config, $modifier);
+        $this->locations[] = compact('location', 'modifier', 'config');
+    }
+
+    public function addServer(array $config)
+    {
+        $this->trigger(self::EVENT_SERVER_ADD_BEFORE, $config);
+        $this->servers[] = compact('config');
+    }
+
+    public function addUpstream(string $upstream, array $config)
+    {
+        $this->trigger(self::EVENT_UPSTREAM_ADD_BEFORE, $upstream, $config);
+        $this->upstreams[] = compact('upstream', 'config');
+    }
+
+    public function dump(): string
+    {
+        $tempName = tempnam(sys_get_temp_dir(), 'nginx');
+        file_put_contents($tempName, implode("\n", [
+            ...array_map(
+                fn($upstream) => $this->write(
+                    "upstream {$upstream['upstream']}",
+                    $upstream['config']
+                ),
+                $this->upstreams
+            ),
+            ...array_map(
+                fn($server) => $this->write(
+                    'server',
+                    array_merge(
+                        $server['config'],
+                        array_map(
+                            fn($location) => $this->write(
+                                "location {$location['modifier']} {$location['location']}",
+                                $location['config']
+                            ),
+                            $this->locations
+                        )
+                    )
+                ),
+                $this->servers
+            ),
+        ]));
+        $result = (string) Scope::fromFile($tempName);
+        unlink($tempName);
+
+        return $result;
+    }
+
+    public function __toString(): string
+    {
+        return $this->dump();
+    }
+
+    public function write(string $index, array $content): string
+    {
+        return sprintf(
+            '%s { %s }',
+            $index,
+            implode("\n", array_map(
+                fn (string $what): string => rtrim(trim($what), ';') . ';',
+                $content
+            ))
+        );
+    }
+}
\ No newline at end of file
diff --git a/lib/DockerComposeCommand.php b/lib/DockerComposeCommand.php
new file mode 100644
index 0000000..adc8636
--- /dev/null
+++ b/lib/DockerComposeCommand.php
@@ -0,0 +1,34 @@
+<?php
+namespace Studip\Dockerized;
+
+use Studip\Dockerized\Traits\GetCompiledDockerComposeYMLPath;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Process\Process;
+
+abstract class DockerComposeCommand extends Command
+{
+    use GetCompiledDockerComposeYMLPath;
+
+    abstract protected function getDockerComposeCommand(): array;
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $process = new Process([
+            'docker', 'compose',
+            '-f', self::GetCompiledDockerComposeYMLPath(),
+            ...$this->getDockerComposeCommand(),
+        ]);
+        $process->setTty(true);
+        $process->mustRun(function ($type, $buffer) use ($output) {
+            if (Process::OUT === $type) {
+                $output->write($buffer);
+            } else {
+                $output->write('<error>' . $buffer . '</error>');
+            }
+        });
+
+        return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
+    }
+}
\ No newline at end of file
diff --git a/lib/SupportedPHPVersions.php b/lib/SupportedPHPVersions.php
new file mode 100644
index 0000000..4db0926
--- /dev/null
+++ b/lib/SupportedPHPVersions.php
@@ -0,0 +1,35 @@
+<?php
+namespace Studip\Dockerized;
+
+final class SupportedPHPVersions
+{
+    private const CONFIGURATION = [
+        '7.2' => 8072,
+        '7.3' => 8073,
+        '7.4' => 8074,
+        '8.0' => 8080,
+        '8.1' => 8081,
+        '8.2' => 8082,
+        '8.3' => 8083,
+    ];
+
+    private const DEFAULT = ['7.4', '8.1', '8.3'];
+
+    public static function getVersions(): array
+    {
+        return array_keys(self::CONFIGURATION);
+    }
+
+    public static function getDefaultVersions(): array
+    {
+        return self::DEFAULT;
+    }
+
+    public static function mapPort(string $version): int
+    {
+        if (!isset(self::CONFIGURATION[$version])) {
+            throw new \Exception('Unsupported php version');
+        }
+        return self::CONFIGURATION[$version];
+    }
+}
\ No newline at end of file
diff --git a/lib/Traits/GetCompiledDockerComposeYMLPath.php b/lib/Traits/GetCompiledDockerComposeYMLPath.php
new file mode 100644
index 0000000..0c04b92
--- /dev/null
+++ b/lib/Traits/GetCompiledDockerComposeYMLPath.php
@@ -0,0 +1,17 @@
+<?php
+namespace Studip\Dockerized\Traits;
+
+trait GetCompiledDockerComposeYMLPath
+{
+    protected static function GetCompiledDockerComposeYMLPath(): string
+    {
+        $filename = realpath(__DIR__ . '/../../compiled/docker-compose.yml');
+        if (!file_exists($filename)) {
+            throw new \Exception('docker-compose.yml was not yet compiled');
+        }
+        if (!is_readable($filename)) {
+            throw new \Exception('docker-compose.yml is not readable');
+        }
+        return $filename;
+    }
+}
\ No newline at end of file
diff --git a/studip-docker b/studip-docker
new file mode 100755
index 0000000..a1bb30e
--- /dev/null
+++ b/studip-docker
@@ -0,0 +1,32 @@
+#!/usr/bin/env php
+<?php
+require __DIR__ . '/vendor/autoload.php';
+
+use Studip\Dockerized\Commands;
+use Symfony\Component\Console\Application;
+
+const CONFIG_FILE = __DIR__ . '/config.json';
+
+\Studip\Dockerized\Config::load(CONFIG_FILE);
+
+$application = new Application('Stud.IP Dockerized', '1.0');
+
+$application->add(new Commands\Init());
+
+if (file_exists(CONFIG_FILE)) {
+    $application->add(new Commands\Compile());
+
+    $application->add(new Commands\Sites\Add());
+    $application->add(new Commands\Sites\Remove());
+    $application->add(new Commands\Sites\Show());
+
+    $application->add(new Commands\PHP\Disable());
+    $application->add(new Commands\PHP\Enable());
+    $application->add(new Commands\PHP\Port());
+
+    $application->add(new Commands\Docker\Build());
+    $application->add(new Commands\Docker\Start());
+    $application->add(new Commands\Docker\Stop());
+}
+
+$application->run();
diff --git a/web/index.php b/web/index.php
new file mode 100644
index 0000000..8169b24
--- /dev/null
+++ b/web/index.php
@@ -0,0 +1,48 @@
+<?php
+$config = json_decode(file_get_contents(__DIR__ . '/../studip-dockerized-config.json'), true);
+?>
+<!doctype html>
+<html>
+<head>
+
+</head>
+<body>
+<style>
+html {
+    font-size: 4em;
+}
+table {
+    width: 100%;
+}
+th {
+    text-align: left;
+}
+</style>
+<?php if (empty($config['sites'])): ?>
+    No sites defined
+<?php else: ?>
+    <table>
+        <thead>
+            <tr>
+                <th>Name</th>
+                <th colspan="<?= count($config['php']) ?>"></th>
+            </tr>
+        </thead>
+        <tbody>
+        <?php foreach ($config['sites'] ?? [] as $path => $definition): ?>
+            <tr>
+                <td><?= htmlentities($path) ?></td>
+            <?php foreach ($config['php'] as $version => $port): ?>
+                <td>
+                    <a href="http://localhost:<?= $port ?>/<?= htmlentities($path) ?>">
+                        <?= htmlentities($version) ?>
+                    </a>
+                </td>
+            <?php endforeach; ?>
+            </tr>
+        <?php endforeach; ?>
+        </tbody>
+    </table>
+<?php endif; ?>
+</body>
+</html>
-- 
GitLab