Nginx + PHP-FPM: Neue Webseite automatisiert erstellen

Letztes Mal hatte ich euch gezeigt, wie man relativ einfach voneinander getrennte Auftritte mit Nginx und PHP-FPM bereitstellen kann. Eine Sache war euch jedoch sicherlich aufgefallen: Das Anlegen und Konfigurieren benötigt viele Schritte, die sich für jede neue Webseite wiederholen.

Und da es mir auch zu aufwändig ist, soetwas immer und immer wieder händisch zu erledigen, habe ich mir das ganze durch ein kleines Script automatisiert. Geschrieben habe ich es in PHP (ist sowieso auf dem System vorhanden und vor allem für solche Kleinigkeiten gut geeignet). :-)

Ich habe das ganze in 3 Dateien aufgeteilt: Ein Script zum Anlegen von statischen Webseiten (ohne PHP), ein Script zum Anlegen von dynamischen Inhalten (mit PHP) und ein Script, in dem die ganzen Hilfsfunktionen enthalten sind. Solchen PHP-Scripten gebe ich immer die Endung .phs um sie nicht mit Webscripten vertauschen zu können. Sie alle habe ich in den Ordner "/var/www/" verfrachtet, da dort auch sämtliche Konfigurationsdateien abgelegt sind. Es reicht übrigens aus, wenn root die Dateien lesen und schreiben kann. ;-)

Die eigentlichen Scripte sind ziemlich kurz geraten. Sie heißen bei mir "make_static.phs" und "make_php.phs" und werden einfach wie folgt aufgerufen:

1
2
sudo php /var/www/make_static.phs example.com
sudo php /var/www/make_php.phs example.net

"example.com" bzw. "example.net" müssen natürlich durch eure tatsächliche Domain ersetzt werden. Die Scripte kümmern sich dann entsprechend um das Anlegen der Konfigurationen, das Anlegen des entsprechenden Nutzers, das Anlegen der benötigten Ordner und das Neustarten der Services.

Hier die "make_static.phs":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
  include("/var/www/make_inc.phs");

  $domain = init($argv);

  if ($domain != NULL) {
    $user = getUserFromDomain($domain);

    createUser($user, false);

    writeNginx($domain, $user, false);

    restartNginx();
  }

  finit($domain)
?>

Und hier die "make_php.phs":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
  include("/var/www/make_inc.phs");

  $domain = init($argv);

  if ($domain != NULL) {
    $user = getUserFromDomain($domain);

    createUser($user, true);

    writeNginx($domain, $user, true);
    writePhp($user);

    restartNginx();
    restartPhp();
  }

  finit($domain)
?>

Und hier nun die "make_inc.phs":

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
<?php
  $HOME_PATH = "/home/sftpuser/";

  $MYSQL_SOCKET = "/var/mysql/socket/mysqld.socket";

  $NGINX_CONF = "/var/www/nginx/";
  $NGINX_PATH = "/etc/init.d/nginx";

  $PHP_CONF    = "/var/www/php-fpm/";
  $PHP_PATH    = "/etc/init.d/php5-fpm";
  $PHP_SOCKETS = "/var/www/sockets/";

  $LIBNSS_PATH    = "/lib/libnss_dns.so.2";
  $LOCALTIME_PATH = "/etc/localtime";
  $RESOLV_PATH    = "/etc/resolv.conf";
  $ZONEINFO_PATH  = "/usr/share/zoneinfo/";

  $CHMOD_CMD    = "chmod";
  $CHMOD_R_CMD  = "chmod -R";
  $CHOWN_CMD    = "chown";
  $CHOWN_R_CMD  = "chown -R";
  $CP_CMD       = "cp";
  $CP_R_CMD     = "cp -R";
  $GROUPMOD_CMD = "usermod -a -G";
  $HOMEMOD_CMD  = "usermod -d";
  $LN_CMD       = "ln";
  $MKDIR_CMD    = "mkdir";
  $MKNOD_CMD    = "mknod -m 666";
  $SHELLMOD_CMD = "usermod -s";
  $USERADD_CMD  = "useradd";

  $NOLOGIN_SHELL = "/usr/sbin/nologin";

  $ADMIN_USER = "[ADMIN]"; //!!! CHANGE THIS
  $NGINX_USER = "nginx";
  $PHP_USER   = "php-fpm";

  $HANDLE_FAVICON = true;
  $HANDLE_MYSQL   = true;
  $HANDLE_ROBOTS  = true;
  $HANDLE_SSL     = true;
  $HANDLE_UBUNTU  = true;
  $HANDLE_WWW     = true;

  function execute($command) {
    $return = "";

    print("\n> $command\n\n");

    system($command, $return);

    print("$return\n");
  }

  function addUserToGroup($user, $group) {
    global $GROUPMOD_CMD;

    execute("$GROUPMOD_CMD $group $user");
  }

  function createUser($user, $usePhp) {
    global $USERADD_CMD;

    global $ADMIN_USER;
    global $NGINX_USER;

    execute("$USERADD_CMD $user");

    addUserToGroup($ADMIN_USER, $user);
    addUserToGroup($NGINX_USER, $user);

    setUserHome($user);
    setUserShell($user);

    createUserHome($user, $usePhp);
  }

  function createUserHome($user, $usePhp) {
    global $HOME_PATH;

    global $CHMOD_CMD;
    global $CHMOD_R_CMD;
    global $CHOWN_CMD;
    global $CHOWN_R_CMD;
    global $CP_CMD;
    global $CP_R_CMD;
    global $LN_CMD;
    global $MKDIR_CMD;
    global $MKNOD_CMD;

    global $NGINX_USER;

    global $MYSQL_SOCKET;

    global $LIBNSS_PATH;
    global $LOCALTIME_PATH;
    global $RESOLV_PATH;
    global $ZONEINFO_PATH;

    global $HANDLE_MYSQL;
    global $HANDLE_UBUNTU;

    $folders = array();
    $folders[] = array($MKDIR_CMD => "",
                       $CHMOD_CMD => "1770",
                       $CHOWN_CMD => "$NGINX_USER:$user");
    $folders[] = array($MKDIR_CMD => "/docs",
                       $CHMOD_CMD => "1770",
                       $CHOWN_CMD => "$NGINX_USER:$user");
    $folders[] = array($MKDIR_CMD => "/home",
                       $CHMOD_CMD => "1700",
                       $CHOWN_CMD => "$user:$user");
    $folders[] = array($MKDIR_CMD => "/logs",
                       $CHMOD_CMD => "1750",
                       $CHOWN_CMD => "$NGINX_USER:$user");

    if ($usePhp) {
      $folders[] = array($MKDIR_CMD => "/bin",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/dev",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/etc",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/lib",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/sbin",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/tmp",
                         $CHMOD_CMD => "1770",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");

      $folders[] = array($MKDIR_CMD => "/usr/bin",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr/lib",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr/local",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr/sbin",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");

      $folders[] = array($MKDIR_CMD => "/usr/local/bin",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr/local/etc",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr/local/lib",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");
      $folders[] = array($MKDIR_CMD => "/usr/local/sbin",
                         $CHMOD_CMD => "1750",
                         $CHOWN_CMD => "$NGINX_USER:$user");


      if ($HANDLE_MYSQL) {
        $folders[] = array($MKDIR_CMD => "/mysql",
                           $CHMOD_CMD => "1750",
                           $CHOWN_CMD => "$NGINX_USER:$user");
      }

      if ($HANDLE_UBUNTU) {
        $folders[] = array($MKDIR_CMD => "/usr/share",
                           $CHMOD_CMD => "1750",
                           $CHOWN_CMD => "$NGINX_USER:$user");
        $folders[] = array($MKDIR_CMD => "/usr/share/zoneinfo",
                           $CHMOD_CMD => "1750",
                           $CHOWN_CMD => "$NGINX_USER:$user");
      }
    }

    foreach ($folders as $folder) {
      execute("$MKDIR_CMD $HOME_PATH$user$folder[$MKDIR_CMD]");
      execute("$CHMOD_CMD $folder[$CHMOD_CMD] $HOME_PATH$user$folder[$MKDIR_CMD]");
      execute("$CHOWN_CMD $folder[$CHOWN_CMD] $HOME_PATH$user$folder[$MKDIR_CMD]");
    }

    if ($usePhp) {
      execute("$MKNOD_CMD $HOME_PATH$user/dev/null c 1 3");
      execute("$MKNOD_CMD $HOME_PATH$user/dev/random c 1 8");
      execute("$MKNOD_CMD $HOME_PATH$user/dev/urandom c 1 9");
      execute("$MKNOD_CMD $HOME_PATH$user/dev/zero c 1 5");
      execute("$CP_CMD $LIBNSS_PATH $HOME_PATH$user$LIBNSS_PATH");
      execute("$CP_CMD $RESOLV_PATH $HOME_PATH$user$RESOLV_PATH");

      if ($HANDLE_MYSQL) {
        execute("$LN_CMD $MYSQL_SOCKET $HOME_PATH$user/mysql/");
      }

      if ($HANDLE_UBUNTU) {
        execute("$CP_CMD $LOCALTIME_PATH $HOME_PATH$user$LOCALTIME_PATH");
        execute("$CP_R_CMD $ZONEINFO_PATH $HOME_PATH$user$ZONEINFO_PATH../");
      }
    }
  }

  function setUserHome($user) {
    global $HOME_PATH;

    global $HOMEMOD_CMD;

    execute("$HOMEMOD_CMD $HOME_PATH$user/home $user");
  }

  function setUserShell($user) {
    global $SHELLMOD_CMD;

    global $NOLOGIN_SHELL;

    execute("$SHELLMOD_CMD $NOLOGIN_SHELL $user");
  }

  function getUserFromDomain($domain) {
    $return = ereg_replace("[.]", "_", $domain);

    print("user  : $return\n");

    return $return;
  }

  function restartNginx() {
    global $NGINX_PATH;

    execute("$NGINX_PATH restart");
  }

  function restartPhp() {
    global $PHP_PATH;

    execute("$PHP_PATH restart");
  }

  function writeNginx($domain, $user, $usePhp) {
    global $HOME_PATH;

    global $CHMOD_CMD;
    global $CHOWN_CMD;

    global $NGINX_CONF;
    global $PHP_SOCKETS;

    global $NGINX_USER;

    global $HANDLE_FAVICON;
    global $HANDLE_ROBOTS;
    global $HANDLE_SSL;
    global $HANDLE_WWW;

    $config  = "server {\n";
    $config .= "  listen 80;\n";

    if ($HANDLE_SSL) {
      $config .= "  listen 443 ssl;\n";
    }

    $config .= "\n";

    if ($HANDLE_WWW) {
      $config .= "  server_name $domain www.$domain;\n";
      $config .= "\n";
      $config .= "  if (\$http_host = www.$domain) {\n";
      $config .= "    rewrite ^(.*)\$ \$scheme://$domain\$1 permanent;\n";
      $config .= "  }\n";
    } else {
      $config .= "  server_name $domain;\n";
    }

    $config .= "\n";
    $config .= "  access_log $HOME_PATH$user/logs/access.log;\n";
    $config .= "  error_log  $HOME_PATH$user/logs/error.log info;\n";
    $config .= "\n";
    $config .= "  root $HOME_PATH$user/docs/;\n";

    if ($usePhp) {
      $config .= "\n";
      $config .= "  # ENABLE PHP\n";
      $config .= "  location ~ ^(.*)\\.php\$ {\n";
      $config .= "    if (!-f \$request_filename) {\n";
      $config .= "      return 404;\n";
      $config .= "    }\n";
      $config .= "\n";
      $config .= "    include      /etc/nginx/fastcgi_params;\n";
      $config .= "    fastcgi_pass unix:$PHP_SOCKETS$user.socket;\n";
      $config .= "    #fastcgi_pass 127.0.0.1:10001;\n";
      $config .= "  }\n";
    }

    $config .= "\n";
    $config .= "  include $NGINX_CONF"."conf.default;\n";

    if ($HANDLE_FAVICON) {
      $config .= "\n";
      $config .= "  location = /favicon.ico {\n";
      $config .= "    if (!-f \$request_filename) {\n";
      $config .= "      return 204;\n";
      $config .= "    }\n";
      $config .= "  }\n";
    }

    if ($HANDLE_ROBOTS) {
      $config .= "\n";
      $config .= "  location = /robots.txt {\n";
      $config .= "    if (!-f \$request_filename) {\n";
      $config .= "      return 204;\n";
      $config .= "    }\n";
      $config .= "  }\n";
    }

    $config .= "\n";
    $config .= "  #deactivate later on\n";
    $config .= "  #autoindex on;\n";
    $config .= "}\n";

    file_put_contents("$NGINX_CONF$user.conf", $config);

    execute("$CHMOD_CMD 660 $NGINX_CONF$user.conf");
    execute("$CHOWN_CMD $NGINX_USER:$NGINX_USER $NGINX_CONF$user.conf");
  }

  function writePhp($user) {
    global $HOME_PATH;

    global $CHMOD_CMD;
    global $CHOWN_CMD;

    global $PHP_CONF;
    global $PHP_SOCKETS;

    global $PHP_USER;

    $config  = "[$user]\n";
    $config .= "listen                 = $PHP_SOCKETS$user.socket\n";
    $config .= ";listen                 = 127.0.0.1:10001\n";
    $config .= "listen.backlog         = -1\n";
    $config .= "listen.allowed_clients = 127.0.0.1\n";
    $config .= "listen.owner           = $user\n";
    $config .= "listen.group           = $user\n";
    $config .= "listen.mode            = 0660\n";
    $config .= "\n";
    $config .= "user  = $user\n";
    $config .= "group = $user\n";
    $config .= "\n";
    $config .= "pm                   = dynamic\n";
    $config .= "pm.max_children      = 5\n";
    $config .= "pm.start_servers     = 2\n";
    $config .= "pm.min_spare_servers = 1\n";
    $config .= "pm.max_spare_servers = 3\n";
    $config .= "pm.max_requests      = 500\n";
    $config .= "\n";
    $config .= "chroot = $HOME_PATH$user\n";
    $config .= "chdir  = /docs\n";
    $config .= "\n";
    $config .= "env[HOSTNAME]      = \$HOSTNAME\n";
    $config .= "env[PATH]          = /usr/local/bin:/usr/bin:/bin\n";
    $config .= "env[TMP]           = /tmp\n";
    $config .= "env[TMPDIR]        = /tmp\n";
    $config .= "env[TEMP]          = /tmp\n";
    $config .= "env[HOME]          = /home\n";
    $config .= "env[DOCUMENT_ROOT] = /docs\n";

    file_put_contents("$PHP_CONF$user.conf", $config);

    execute("$CHMOD_CMD 660 $PHP_CONF$user.conf");
    execute("$CHOWN_CMD $PHP_USER:$PHP_USER $PHP_CONF$user.conf");
  }

  function init($arguments) {
    $return = NULL;

    if (count($arguments) > 1) {
      $return = strtolower(ereg_replace("[^A-Za-z0-9.-]", "", $arguments[1]));

      if (strlen($return) > 0) {
        print("domain: $return\n");
      } else {
        $return = NULL;
      }
    }

    return $return;
  }

  function finit($domain) {
    if ($domain != NULL) {
    } else {
      print("ERROR: domain must not be empty\n");
    }
  }
?>

Der "make_inc.phs" solltet ihr euch ein paar Minuten widmen, da dort ein paar Einstellungen auf euch warten. Die Pfadangaben sollten verständlich sein: Wo finden sich die Init-Scripte, wie lauten die User-Homeverzeichnisse, wo befinden sich die Configs von Nginx und PHP-FPM und wo werden Socket-Dateien abgelegt.
Spannend dürften auch die User-Namen sein. Klar, für jede Webseite wird ein neuer Nutzer angelegt, dessen Name die entsprechende Domain widerspiegelt. Sowohl der Admin (von euch zu ändern!) als auch Nginx werden dessen Nutzergruppe hinzugefügt, um die Dateien erreichen zu können.
Abschließend gibt es noch "HANDLE_FAVICON", "HANDLE_SSL" und "HANDLE_WWW". Ersteres sorgt dafür, dass Browser beim Aufrufen eines nicht-existierenden Favicons keinen 404 erhalten, zweiteres dass der Server SSL bereitstellt und drittes sorgt dafür, dass durch Rewriting vorangestellte "www."-Subdomains verschwinden.

Noch eine Info: In den erstellten Nginx-Konfigurationsdateien wird die Datei "/var/www/nginx/conf.default" inkludiert. In diese habe ich Regeln ausgelagert, die sich bei wirklich jeder Webseite wiederholen. In meinem Fall ist das z.b. das Nichtausliefern von versteckten Dateien und Ordnern:

1
2
3
4
5
rewrite ^(.*)\/\.(.*)$ @404 break;

location = @404 {
  return 404;
}

Ihr werdet es schon gemerkt haben: Diese Scripte sind sehr stark auf meine eigenen Bedürfnisse zugeschnitten. Ja, nicht jeder will seinen Server derart aufsetzen - aber evtl. erhalten ja auch andere Leute ein paar Ideen durch diese Beispielimplementierung. :-)

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:
Das Script wurde an den aktuellen Stand angepasst. Primär sind die Schalter "HANDLE_MYSQL", "HANDLE_ROBOTS" und "HANDLE_UBUNTU" hinzugekommen. HANDLE_MYSQL erzeugt einen Hardlink zum mysqld-Socket im Verzeichnis des entsprechenden Nutzers. HANDLE_ROBOTS arbeitet wie HANDLE_FAVICON und erzeugt eine Regel, durch die bei fehlender "robots.txt" ein 204-Code zurückgeliefert wird. HANDLE_UBUNTU sorgt dafür, dass im PHP Chroot alle Dateien vorliegen, damit PHP ordentlich funktionieren kann. Im speziellen wird dadurch folgender Fehler gelöst:

1
Fatal error: main(): Timezone database is corrupt - this should *never* happen! in [...] on line [...]

Update:
Das Script ist wieder einmal erweitert worden. Die für PHP eingerichtete chroot-Umgebung reicht nun aus, um DNS-Abfragen auszuführen ("/etc/resolv.conf" und "/lib/libnss_dns.so.2") und verschlüsselte Verbindungen herzustellen ("/dev/random" und "/dev/urandom").
Automatisierte Grüße, Kenny