Konfiguration von Nginx + PHP-FPM

Und weiter geht's in der Konfigurations-Reihe. Was wäre schon ein toller, schneller Webserver ohne... ähm... Webserver? Im Gegensatz zum üblichen Standard - also Apache Httpd - wollte ich mal den angeblich schnelleren und Ressourcen-sparenderen russischen Nginx-Webserver ausprobieren. Natürlich benötigt so ein Webserver auch ein bisschen Dynamik - in meinem Fall muss dafür PHP in Form von PHP-FPM herhalten. 🙂

Als Quellen für das von mir verwendete Ubuntu Lucid Lynx habe ich folgende Zeilen in meine "/etc/apt/sources.list" aufgenommen:

1
2
3
4
5
6
7
# nginx
deb http://nginx.org/packages/ubuntu/ lucid nginx
deb-src http://nginx.org/packages/ubuntu/ lucid nginx

# PHP 5
deb http://ppa.launchpad.net/nginx/php5.3/ubuntu lucid main
deb-src http://ppa.launchpad.net/nginx/php5.3/ubuntu lucid main

Bei Launchpad werden die einzelnen Pakete signiert, um sicher zu gehen, dass ihr sie von der korrekten Quelle herunterladet. Aus diesem Grund muss man sich nun erst einmal den passenden Schlüssel besorgen. Auf der Archivseite findet man die Schlüssel-ID (derzeit "8B3981E7A6852F782CC4951600A6F0A3C300EE8C"). Diesen Schlüssel holt man sich nun mit folgendem Befehl vom Ubuntu-Schlüsselserver:

1
sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 8B3981E7A6852F782CC4951600A6F0A3C300EE8C

Anschließend kann man mit folgenden Befehlen die Software installieren (Befehle sind beispielhaft für Ubuntu). Da man eh gerade dabei ist, kann man der PHP-Installation auch gleich noch die Suhosin-Erweiterung und die MySQL-Unterstützung (für später) spendieren:

1
2
3
apt-get update
apt-get install nginx
apt-get install php5-fpm php5-suhosin php5-mysql

Anschließend geht es ans Eingemachte. Als allererstes führen wir die Basiskonfiguration von Nginx in der Datei "/etc/nginx/nginx.conf" durch. Dort definieren wir Dinge wie die Anzahl der Arbeitsprozesse, Log-Dateinamen, wir aktivieren die Kompression des Outputs und definieren, wo wir die Konfigurationsdateien der einzelnen Hosts plazieren:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
user             nginx nginx;
worker_processes 4;
error_log        /var/log/nginx/error.log info;
pid              /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include      /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  /var/log/nginx/access.log main;

    # GENERAL
    ignore_invalid_headers  on;
    sendfile                on;
    server_name_in_redirect off;
    server_tokens           off;

    # TCP
    tcp_nodelay off;
    tcp_nopush  on;

    # TIMEOUTS
    client_body_timeout   65;
    client_header_timeout 65;
    keepalive_timeout     65 65;
    send_timeout          65;

    # COMPRESSION
    gzip              on;
    gzip_static       on;
    gzip_buffers      256 8k;
    gzip_comp_level   9;
    gzip_http_version 1.0;
    gzip_min_length   0;
    gzip_types        text/css text/javascript text/mathml text/plain text/xml application/x-javascript application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml;
    gzip_vary         on;
    gzip_disable      "MSIE [1-6]\.(?!.*SV1)";

    index index.html index.htm index.php;

    # HOSTS
    include /var/www/nginx/*.conf;
}

Zuerst einmal solltet ihr die Konfiguration "user nginx nginx;" beachten - diese wird später noch wichtig werden, da der User "nginx" alle Dateien erreichen können muss, die später ausgeliefert werden sollen.
Desweiteren sind noch ein paar kleine Sicherheitseinstellungen definiert. Zum einen, das unbekannte Header ignoriert werden sollen und, dass in den Antworten die Serverversion nicht mit ausgegeben werden soll. Die Einstellung "gzip_types" muss evtl. noch um weitere Mime-Typen ergänzt werden (dort steht drin, welche Dateiinhalte alles komprimiert übertragen werden sollen).

Nun sollten wir noch die Datei "/etc/nginx/fastcgi_params" ein bisschen erweitern, um uns später die Arbeit zu erleichtern. Bei mir sieht diese wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fastcgi_index index.php;

fastcgi_connect_timeout 65;
fastcgi_send_timeout    180;
fastcgi_read_timeout    180;

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  DOCUMENT_ROOT      /docs;
fastcgi_param  SCRIPT_FILENAME    /docs$fastcgi_script_name;
fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

Als nächstes können wir nun unseren Default-Auftritt definieren. Dieser wird immer dann verwendet, wenn keine passendere Definition gefunden wird (z.B. wenn der Webserver über die IP-Adresse aufgerufen wird). Wir erstellen also die Datei "/var/www/nginx/_.conf" und befüllen sie wie folgt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
  listen      80 default;
  server_name _;

  access_log /var/www/default/logs/access.log;
  error_log  /var/www/default/logs/error.log info;

  root /var/www/default/docs/;

  # HIDDEN FILES AND FOLDERS
  rewrite ^(.*)\/\.(.*)$ @404 break;

  location = @404 {
    return 404;
  }

  #deactivate later on
  #autoindex on;
}

Das Definieren eines solchen Auftritts ist relativ einfach: Portnumer, Hostname, Logdateien und Wurzelverzeichnis definieren. Fertig! Jetzt nurnoch die Ordner "/var/www/default/logs/" und "/var/www/default/docs/" anlegen. In den docs-Ordner kommen die Dateien, die online aufrufbar sein sollen und fertig ist der Default-Auftritt! 😀

Statische Webseiten betreiben zu können ist zwar gut und schön (und mit den bisherigen Mitteln auch möglich), aber wirklich spannend wird es doch erst mit dynamischen Webseiten. Deshalb teilen wir PHP-FPM nun im ersten Schritt mit, wo es die Konfigurationsdateien für die einzelnen Auftritte findet. Dazu öffnen wir die Datei "/etc/php5/fpm/main.conf" und ändern einfach einen einzelnen Wert wie folgt:

1
include=/var/www/php-fpm/*.conf

Danach legen wir noch den Ordner "/var/www/sockets/" an, der durch den Nutzer "nginx" lesbar sein muss.

Und jetzt kommt die Magie zum tragen. Wir stellen und vor, wir möchten einen Auftritt "example.com" anlegen. Dieser benötigt eine Nginx-Konfiguration, eine PHP-FPM-Konfiguration und ein paar Verzeichniss.

Zuerst die Nginx-Konfiguration: Wir erstellen eine Datei "/var/www/nginx/example_com.conf" und füllen sie mit folgendem Inhalt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server {
  listen      80;
  server_name example.com;

  access_log /home/sftpuser/example_com/logs/access.log;
  error_log  /home/sftpuser/example_com/logs/error.log info;

  root /home/sftpuser/example_com/docs;

  # HIDDEN FILES
  location ~ ^(.*)\/\.(.*)$ {
    return 404;
  }

  # ENABLE PHP
  location ~ ^(.*)\.php$ {
    if (!-f $request_filename) {
      return 404;
    }

    include       /etc/nginx/fastcgi_params;
    fastcgi_pass  unix:/var/www/sockets/example_com.socket;
    #fastcgi_pass  127.0.0.1:10001;
  }

  #deactivate later on
  #autoindex on;
}

Puh! Hier muss ein bisschen erklärt werden. Klar, wir definieren wieder Port, Hostname, Logdateien und Wurzelverzeichnis. Die Ordnernamen entstehen durch die Art, wie ich meinen SSH-Server eingerichtet habe. Wie man also sieht, erhält jeder Auftritt seinen eigenen Systemnutzer. Dadurch soll verhindert werden, dass durch eine kompromitierte Webseite alle anderen Auftritte ebenfalls befallen werden können.
Und weiter: Im nächsten Schritt definieren wir die PHP-Nutzung. Alle Dateien, die mit ".php" enden und existieren, sollen an den PHP-Interpreter weitergegeben werden. Dieser wird später über einen Unix-Socket erreichbar sein (ist schneller) und wird mit den Parametern aus der "fastcgi_params"-Datei gefüttert.

Und nun kommt die PHP-Konfiguration - diese legen wir in der Datei "/var/www/php-fpm/example_com.conf" ab:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[example_com]
listen = /var/www/sockets/example_com.socket
listen.owner = example_com
listen.group = example_com
listen.mode = 0666

listen.backlog = -1
listen.allowed_clients = 127.0.0.1

user = example_com
group = example_com

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500

chroot = /home/sftpuser/example_com
chdir = /docs
 
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
env[HOME] = /home
env[DOCUMENT_ROOT] = /docs

Und noch mehr Erklärungen: Wir definieren hier einen Pool an PHP-Prozessen, den wir "example_com" nennen. Dieser Pool soll über den Unix-Socket "/var/www/sockets/example_com.socket" erreichbar sein (der gleiche, wie in der Nginx-Konfiguration). Die Socket-Datei soll vom entsprechenden Nutzer ("example_com") gelesen werden können. Die zu startenden PHP-Prozesse selbst sollen ebenfalls unter diesem Nutzer laufen. Danach wird definiert, wieviele Prozesse laufen, nach wievielen Aufrufen sie neu gestartet werden sollen und so weiter.
Nun kommt das spannende: Der Nutzer "example_com" wird in seinem eigenen Verzeichnis eingesperrt (gechrootet). Damit soll verhindert werden, dass er überhaupt in die Ordner von anderen Auftritten hineingucken kann. Dafür werden abschließend noch ein paar Variablen verbogen. 🙂

Sooo... jetzt sind nur noch ein paar Schritte notwending:

  • Der Nutzer "example_com" muss angelegt werden.
  • Der Nutzer "nginx" muss der Gruppe "example_com" zugeordnet werden.
  • Folgende Ordner müssen angelegt und für den Nutzer "example_com" zugänglich gemacht werden:
    • /home/sftpuser/example_com
    • /home/sftpuser/example_com/bin
    • /home/sftpuser/example_com/docs
    • /home/sftpuser/example_com/home
    • /home/sftpuser/example_com/logs
    • /home/sftpuser/example_com/tmp
    • /home/sftpuser/example_com/usr/bin
    • /home/sftpuser/example_com/usr/local/bin
  • Nginx muss mit dem Befehl "/etc/init.d/nginx restart" neu gestartet werden.
  • PHP-FPM muss mit dem Befehl "/etc/init.d/php-fpm restart" neu gestartet werden.

Ja, es sieht im ersten Moment nach viel aus, aber: Im Grunde müssen für einen weiteren Auftritt nur 2 kleine Config-Dateien nach Schema F erstellt werden, die Ordnerstruktur angelegt, ein paar Rechte gesetzt und die Dienste neu gestartet werden. Das alles lässt sich relativ mühelos durch Scripte automatisieren. Genau das werde ich wahrscheinlich morgen in Angriff nehmen - Scripte, um neue Auftritte anzulegen.

Bis dahin hoffe ich, dass diese geballte Sammlung an Konfigurationstipps dem ein oder anderen Suchenden hilfreich zur Seite stehen werden. Ich habe eine halbe Ewigkeit gesessen, um alle Informationen zusammen zu sammeln und miteinander zu kombinieren. 🙂

Und wie immer gilt: Ich hafte nicht für Schäden an Software, Hardware oder für Vermögensschäden, die durch Anwendung dieser Änderungen entstanden sind oder entstehen könnten. 😉
Update:
Wie ich durch einen der Kommentare mitbekommen habe, wird das Launchpad-Repository für PHP 5.3 nicht mehr zur Verfügung gestellt. Michael Lustfield - der Besitzer des entsprechenden Repositories - hat diesen Schritt in seinem Blog damit begründet, dass es nie vorgesehen war, dass das PHP-Repository von so vielen Leuten genutzt wird und so viel Zeit verbraucht, wie nötig wäre, um das Repository up-to-date zu halten. Schade. Eine alternative Anlaufstelle ist z.B. das Repository von DotDeb.org.
Dynamische Grüße, Kenny

13 Kommentare » Schreibe einen Kommentar

  1. Pingback: A fast and secure web server with nginx on a Raspberry Pi « GartenEden

  2. Pingback: Sicherer und schneller Webserver mit nginx auf einem Raspberry Pi « Garten Eden

  3. Die Lösung ist ja schön und gut, auch wunderbar geschrieben.
    Für lighttpd ist das vorgehen übrigens fast genauso.

    Nur wie schaut es aus wenn man über 100 Webseiten hat und im laufe des Tages so an die 20 neue Webseiten dazu schalten muss. Immer wieder den Webserver dafür neustarten? Gibt es keine Möglichkeit dies mit einem Reload zu lösen? Oder kennt jemand eine chroot Möglichkeit, ohne den Webserver neustarten zu müssen?

    • Hallo Patrick. Mit ein bisschen Googlen findet man folgendes:

      Nginx kann man mit einem entsprechenden Kill-Signal dazu bringen, die neue Config einzulesen und beim Neustarten der Kindprozesse diese mit der neuen Config zu versorgen:

      1
      kill -HUP `cat /var/run/nginx.pid`

      Das gleiche gibt es auch für PHP-FPM:

      1
      kill -SIGUSR2 `cat /var/run/php5-fpm.pid`
    • Deshalb sieht die gezeigte Lösung die Nutzung von UNIX-Sockets auch standardmäßig vor. 🙂
      TCP-Sockets werden dann spannend, wenn NGINX auf einem anderen System läuft, als der PHP Interpreter.

    • Also Nginx wird upgedatet. Ordentliche (und aktuelle) PHP-Pakete für Ubuntu zu finden ist jedoch in der Tat ein Problem. Wenn du eine bessere Quelle kennst: immer her damit! 🙂

  4. So, ich habe jetzt noch ein paar Optimierungen vorgenommen auf Basis von neuen Erkenntnissen. Zum einen werden nun versteckte Dateien nicht mehr ausgeliefert (unter Linux werden Dateien durch voranstellen eines Punktes an den Dateinamen versteckt). Zudem wurde der Wert "fastcgi_index" in die "fastcgi_params" ausgegliedert.

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.