ZURÜCK

nginx + symfony als Unterverzeichnis

Konfiguration von nginx mit einer symfony app als Unterverzeichnis.

Datum: 2020-05, Tags: Linux, nginx, symfony, subdirectory, nested

Aus unerfindlichen Gründen ist es unerwartet kompliziert, nginx so zu konfigurieren, dass Webseiten bzw. apps als Unterverzeichnis betrieben werden. Damit meine ich speziell symfony-apps mit php-fpm. Die Motivation für eine derartige Konfiguration kann durchaus unterschiedlich sein, z.B. weil man "unwichtige" webseiten mit nur einem SSL-Zertifikat betreiben will.

Ich will die dafür notwendigen EInstellungen hier einmal durchgehen und beschreiben, warum spezielle Einstellungen so gemacht werden. Mein Setup soll wie folgt aussehen:

"default" Instanz

Los geht's. Erst einmal trage ich den gewünschten hostname in die Datei /etc/hosts ein. Ich könnte auch mit dem hostnamen localhost arbeiten, das kommt mir aber eventuell mit anderen Konfigurationen in die Quere.

/etc/hosts
...
127.0.0.1       apps.local
...

Dann erzeuge ich dafür das Basisverzeichnis und erstelle dort eine Datei index.html. Um sicherzustellen, dass darin auch Unterverzeichnisse richtig verwendet werden, verweise ich auf eine CSS-Datei in /css/style.css.

~mkdir /var/www/apps.local
~cd /var/www/apps.local
/var/www/apps.local/index.html
<html>
<head>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <h1>
  Hello apps!
  </h1>
</body>
</html>
/var/www/apps.local/css/style.css
html {
    background-image: linear-gradient(160deg, #dfe8dd 0%, #1fc8db 51%, #2cb5e8 75%);
    color: #fff;
    text-align: center;
    height: 100%;
    margin: 0;
}
h1 {
    line-height: 5em;
}

Dafür wird eine ganz einfache nginx-Konfiguration benötigt. Ich benutze das conf.d Verzeichnis von nginx, könnte aber auch sites-available benutzen und die conf-Datei dann nach sites-enabled verlinken. Für die nginx-Konfiguration werden dann doch root-Berechtigungen benötigt.

/etc/nginx/conf.d/apps.local.conf
server {
    server_name apps.local;
    root /var/www/apps.local;

    index index.html index.php;

    location / {
        try_files $uri /index.html;
    }

    error_log /var/log/nginx/apps.local_error.log;
    access_log /var/log/nginx/apps.local_access.log;
}

Nach jeder Änderung an der nginx-Konfiguration muss der Dienst mit sudo service nginx reload neu geladen werden, das werde ich im Weiteren nicht mehr extra erwähnen! Nachdem nginx neu geladen wurde, ist meine "default" Instanz nun unter http://apps.local/ erreichbar und auch das css wird richtig verarbeitet. Mehr brauche und will ich nicht an dieser Stelle.

"subdir" Instanz

Ich erzeuge eine einfache symfoy Installation in das Verzeichnis /var/www/apps.local.subdir. Dort will ich für dieses Beispiel lediglich einen einfachen Controller benutzen, sowie assets über "webpack" einbinden. Die Installation kürze ich hier ein wenig ab, die Symfony Dokumentation beschreibt das im Detail. Ich denötige weiterhin das Maker Bundle, Webpack Encore, Twig, sowie den Web Profiler.

/var/www/apps.localmkdir /var/www/apps.local.subdir
/var/www/apps.localcd /var/www
/var/wwwsymfony new apps.local.subdir
* Creating a new Symfony project with Composer
  (running /usr/bin/composer create-project symfony/skeleton /var/www/apps.local.subdir)

* Setting up the project under Git version control
  (running git init /var/www/apps.local.subdir)

 [OK] Your project is now ready in /var/www/apps.local.subdir

/var/wwwcd apps.local.subdir
/var/www/apps.local.subdircomposer require symfony/maker-bundle symfony/webpack-encore-bundle symfony/twig-pack symfony/profiler-pack
Using version ^1.18 for symfony/maker-bundle
Using version ^1.7 for symfony/webpack-encore-bundle
Using version ^1.10 for doctrine/annotations
Using version ^1.0 for symfony/twig-pack
Using version ^1.0 for symfony/profiler-pack
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Restricting packages listed in "symfony/symfony" to "5.0.*"
Package operations: 16 installs, 0 updates, 0 removals
  - Installing nikic/php-parser (v4.4.0): Loading from cache
  - Installing doctrine/inflector (1.4.1): Loading from cache
  - Installing symfony/maker-bundle (v1.18.0): Loading from cache
  - Installing twig/twig (v3.0.3): Loading from cache
  - Installing symfony/translation-contracts (v2.0.1): Loading from cache
  - Installing symfony/twig-bridge (v5.0.8): Loading from cache
  - Installing symfony/twig-bundle (v5.0.8): Loading from cache
  - Installing twig/extra-bundle (v3.0.3): Loading from cache
  - Installing symfony/twig-pack (v1.0.0): Loading from cache
  - Installing symfony/web-profiler-bundle (v5.0.8): Loading from cache
  - Installing symfony/stopwatch (v5.0.8): Loading from cache
  - Installing symfony/profiler-pack (v1.0.4): Loading from cache
  - Installing doctrine/lexer (1.2.0): Loading from cache
  - Installing doctrine/annotations (1.10.2): Loading from cache
  - Installing symfony/asset (v5.0.8): Loading from cache
  - Installing symfony/webpack-encore-bundle (v1.7.3): Loading from cache
Writing lock file
Generating autoload files
Symfony operations: 6 recipes (872bda013e9fd67f074fc6bf021f1c62)
  - Configuring symfony/maker-bundle (>=1.0): From github.com/symfony/recipes:master
  - Configuring symfony/twig-bundle (>=5.0): From github.com/symfony/recipes:master
  - Configuring twig/extra-bundle (>=v3.0.3): From auto-generated recipe
  - Configuring symfony/web-profiler-bundle (>=3.3): From github.com/symfony/recipes:master
  - Configuring doctrine/annotations (>=1.0): From github.com/symfony/recipes:master
  - Configuring symfony/webpack-encore-bundle (>=1.6): From github.com/symfony/recipes:master
Executing script cache:clear [OK]
Executing script assets:install public [OK]

Some files may have been created or updated to configure your new packages.
Please review, edit and commit them: these files are yours.

/var/www/apps.local.subdirphp bin/console make:controller

 Choose a name for your controller class (e.g. VictoriousPuppyController):
 > Default

 created: src/Controller/DefaultController.php
 created: templates/default/index.html.twig

  Success!

 Next: Open your new controller class and add some pages!

Super! Damit sollte ich nun eine Route /default eingerichtet haben, mit einer dazugehörigen rudimentären Seite, deren template in templates/default/index.html.twig liegt. Das wollen wir uns später noch etwas näher ansehen, erst einmal müssen wir aber nginx anpassen, damit wir überhaupt etwas sehen.

Die nginx-Konfigurationsdatei muss um einen location Block erweitert werden:

/etc/nginx/conf.d/apps.local.conf
server {
    server_name apps.local;
    root /var/www/apps.local;

    index index.html index.php;

    location / {
        try_files $uri /index.html;
    }

    set $subdir_root /var/www/apps.local.subdir/public;
    location /subdir {
        alias $subdir_root;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # return 404 for all other php files.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/apps.local_error.log;
    access_log /var/log/nginx/apps.local_access.log;
}

Achtung: Das ist noch nicht die finale Version. Wichtig ist, dass wir in dem location Block ein alias benutzen, nicht noch einmal root, denn sonst würde eine URL wie http://apps.local.subdir/subdir/index.php zu dem Pfad /var/www/apps.local.subdir/public/subdir/index.php aufgelöst werden.

Wenn wir nun die "subdir" Instanz URL http://apps.local/subdir/ im browser benutzen, bekommen wir allerdings eine Fehlermeldung:

404 Not Found
nginx/1.16.1

Und in dem logfile /var/log/nginx/apps.local_access.log finden wir den Eintrag:

127.0.0.1 - - [19/May/2020:10:43:13 +0200] "GET /subdir HTTP/1.1" 404 153 "-" ...

Das liegt daran, dass der Block mit dem "404" Fehler am Ende der Konfiguration greift. Ohne den würde die statische Seite der "default" Instanz ausgegeben werden. Der "404" Block greift für alle php Dateien, das ist aber nicht unsere Intention. Wir wollen PHP an php-fpm übergeben, haben jedoch php-fpm noch nicht eingebunden und müssen die Konfiguration erst einmal nachziehen. Die internal Direktive lassen wir vorerst weg, da sie uns beim debugging stört. In der finalen Version wird sie jedoch wieder enthalten sein.

/etc/nginx/conf.d/apps.local.conf
server {
    server_name apps.local;
    root /var/www/apps.local;

    index index.html index.php;

    location / {
        try_files $uri /index.html;
    }

    set $subdir_root /var/www/apps.local.subdir/public;
    location /subdir {
        alias $subdir_root;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_param APP_ENV dev;
        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/index.php/some-path
        # Remove the internal directive to allow URIs like this
        # internal;
    }

    # return 404 for all other php files.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/apps.local_error.log;
    access_log /var/log/nginx/apps.local_access.log;
}

Wenn wir nun die Seite http://apps.local/subdir/ erneut aufrufen, erhalten wir wieder einen 404-Fehler, diesmal sieht er jedoch etwas anders aus. Im browser steht die Fehlermeldung:

File not found.

In /var/log/nginx/apps.local_access.log steht wiederum:

127.0.0.1 - - [19/May/2020:11:24:52 +0200] "GET /subdir HTTP/1.1" 404 27 "-" ...

Jedoch in /var/log/nginx/apps.local_error.log finden wir nun die php-fpm Fehlermeldung:

2020/05/19 11:25:22 [error] 4728#4728: *1 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 127.0.0.1, server: apps.local, request: "GET /subdir HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.2-fpm.sock:", host: "apps.local"

Der Fehler liegt nun darin, dass der Parameter SCRIPT_FILENAME auf $realpath_root$fastcgi_script_name zeigt. Dabei löst $realpath_root auf das Verzeichnis auf, das als root angegeben wird. In der Dokumentation liest sich das für mich so, als müsste das auch innerhalb einer eine Alias-Konfiguration funktionieren. Das tut es aber nicht wie erwartet, es ist hier notwendig, die Variable $request_filename zu benutzen. Die Konfiguration für php wird in dem location Block verschachtelt, so dass mehrere "subdir" Instanzen unterschiedlich konfiguriert werden können. Der ursprüngliche location Block für php wird nun nicht mehr gebraucht und entfernt. Er könnte jedoch auch erhalten bleiben, wenn man in der "default" Instanz mit php arbeiten möchte.

/etc/nginx/conf.d/apps.local.conf
server {
    server_name apps.local;
    root /var/www/apps.local;

    index index.html index.php;

    location / {
        try_files $uri /index.html;
    }

    set $subdir_root /var/www/apps.local.subdir/public;
    location /subdir {
        alias $subdir_root;
        try_files $uri $uri/ /index.php$is_args$args;

        location ~ \.php {
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            fastcgi_param DOCUMENT_ROOT $subdir_root;
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
            internal;
        }
    }

    # return 404 for all other php files.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/apps.local_error.log;
    access_log /var/log/nginx/apps.local_access.log;
}

Wenn wir nun die "subdir" Instanz unter http://apps.local/subdir/ aufrufen, bekommen wir erstmals unsere Symfony-Installation zu sehen, wenn auch noch mit Problemen.

Der Web Profiler kann offensichtlich nicht laden. Wenn man den Request mit den debugging tools des browsers untersucht, sieht man auch, dass die Seite eigentlich einen "404" Fehler wirft, dass ist dann jedoch schon innerhalb der symfony app.

Auch die schon eingerichte Route http://apps.local/subdir/default funktioniert nicht, hier sehen wir allerdings wieder den nginx-Fehler vom Anfang. Hingegen http://apps.local/subdir/index.php/default funktioniert, solange wir die internal Direktive nicht eingeschaltet haben.

Innerhalb des try_files Blocks kann offenbar die index.php nicht mehr ermittelt werden, wenn sie nicht explizit angegeben wird. Wenn wir das Verzeichnis public/default anlegen und darin eine Datei index.php anlegen, wird diese ausgegeben (bitte nach einem Test wieder löschen)!

Um das Problem aufzulösen, müssen wir für den try_files Block eine rewrite Direktive einfügen. Den folgenden rewrite könnten wir direkt in dem location Block einbauen, ich benutze jedoch eine named location, um bei dieser Konfiguration übersichtlich zu bleiben, falls mehrere rewrites hinzukommen. Beachte, dass "named locations" nicht verschachtelt werden können und auf "server" Ebene eingestellt werden müssen. Gleichzeitig aktivieren wir die internal Direktive:

/etc/nginx/conf.d/apps.local.conf
server {
    server_name apps.local;
    root /var/www/apps.local;

    index index.html index.php;

    location / {
        try_files $uri /index.html;
    }

    set $subdir_root /var/www/apps.local.subdir/public;
    location /subdir {
        alias $subdir_root;
        try_files $uri $uri/ @subdir;

        location ~ \.php {
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            fastcgi_param DOCUMENT_ROOT $subdir_root;
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
            internal;
        }
    }

    location @subdir {
        rewrite /subdir/(.*)$ /subdir/index.php?/$1 last;
    }

    # return 404 for all other php files.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/apps.local_error.log;
    access_log /var/log/nginx/apps.local_access.log;
}

Spoiler: Dies ist die finale Version.

Mit dieser Konfiguration können wir nun endlich unseren Controller sehen und auch der Web Profiler wird erfolgreich geladen!

Aber wir sind immer noch nicht ganz fertig, ich möchte sicherstellen, dass auch alle CSS und javascript assets ordentlich geladen werden. Dazu passe ich das "base" template templates/base.html.twig an und baue meine CSS styles in assets/css/app.css ein:

/var/www/apps.local.subdir/templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}
            {{ encore_entry_link_tags('app') }}
        {% endblock %}
    </head>
    <body>
    {% block body %}{% endblock %}
    {% block javascripts %}
        {{ encore_entry_script_tags('app') }}
    {% endblock %}
    </body>
</html>
/var/www/apps.local.subdir/assets/css/app.css
html {
	background-image: linear-gradient(160deg, #efedf8 0%, #1fdbc8 51%, #2ce8b5 75%);
	height: 100%;
	margin: 0;
}

Dann müssen wir noch die Webpack-Konfiguration entsprechend diesen FAQ and Common Issues anpassen und die assets erzeugen:

/var/www/apps.local.subdir/webpack.config.js
...
// public path used by the web server to access the output path
.setPublicPath('/subdir/build')
// only needed for CDN's or sub-directory deploy
.setManifestKeyPrefix('build/')
...
/var/www/apps.local.subdirnpm install
...
/var/www/apps.local.subdiryarn encore dev
yarn run v1.22.4
$ /var/www/apps.local.subdir/node_modules/.bin/encore dev
Running webpack ...

 DONE  Compiled successfully in 476ms

 I  3 files written to public/build
Entrypoint app = runtime.js app.css app.js
Done in 1.35s.

Nun zeigt die Seite auch meine CSS styles an. Auch über die developer tools im browser sehen wir, dass alle assets richtig geladen werden. Ebenfalls die "default" Instanz funktioniert noch wie erwartet.

Zusammenfassung

Ich finde, dass das deutlich komplizierter einzustellen ist, als ich erwartet hätte.