Миграция на nginx с apache-2.4, или путь по граблям и success-story в финале.

Миграция на nginx с apache-2.4, или путь по граблям и success-story в финале.

Предыстория сводится к тому, что был почтовый сервер, на котором крутился apache-2.4 + PHP + кое-какие CGI-скрипты. При очередном апдейте системы возникло острое желание заменить индейца на nginx, плюс прикрутить к серверу rspamd и получить www-интерфейс к последнему.

В ходе этой работы я потоптался по некоторым граблям, методы борьбы с которыми я и опишу ниже.

Базовые настройки

Просто, чтобы иметь определённый фундамент для последующего описания, указываю «начальные» настройки системы.

/etc/php/fpm-php7.0/fpm.d/mail.conf
[mail]
listen = /run/php-fpm.mail.socket
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
user = nginx
group = nginx
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 10
/etc/nginx/nginx.conf
user nginx nginx;
worker_processes 2;
 
error_log /var/log/nginx/error_log info;
 
events {
        worker_connections 64;
        use epoll;
}
 
http {
        include /etc/nginx/mime.types;
        default_type application/octet-stream;
 
        log_format main
                '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '"$gzip_ratio"';
 
        client_header_timeout 10m;
        client_body_timeout 10m;
        send_timeout 10m;
        client_max_body_size 100m;
 
        connection_pool_size 256;
        client_header_buffer_size 1k;
        large_client_header_buffers 4 2k;
        request_pool_size 4k;
 
        proxy_buffers 8 64k;
        proxy_intercept_errors on;
        proxy_connect_timeout 1s;
        proxy_read_timeout 3s;
        proxy_send_timeout 3s;
 
        gzip on;
        gzip_min_length 1100;
        gzip_buffers 4 8k;
        gzip_types text/plain;
 
        output_buffers 1 32k;
        postpone_output 1460;
 
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 75 20;
        ignore_invalid_headers on;
 
        include /etc/nginx/sites/*.conf;
}
/etc/nginx/sites/mail.somedomain.ru.conf
server {
        server_name
                mx.somedomain.ru
                mail.somedomain.ru
        ;
        listen 80;
        rewrite ^ https://mail.somedomain.ru$request_uri? permanent;  # enforce https
}
 
server {
        server_name
                mx.somedomain.ru
                mail.somedomain.ru
        ;
        listen 443 ssl http2;
        autoindex off;
        gzip off;
        client_max_body_size 15M;
        client_body_buffer_size 128k;
        index index.html index.htm index.php;
        access_log  /var/log/nginx/mail.somedomain.ru-access.log;
        error_log   /var/log/nginx/mail.somedomain.ru-error.log;
 
        set  $www_root /var/www/mail.somedomain.ru/htdocs;
        root $www_root;
 
        ssl on;
        ssl_certificate      /etc/ssl/wildcard/wildcard.somedomain.ru.pem;
        ssl_certificate_key  /etc/ssl/wildcard/wildcard.somedomain.ru.key;
#        ssl_dhparam          /etc/letsencrypt/live/mail.somedomain.ru/dhparam.pem;
        resolver             1.2.3.4;                       
#        ssl_stapling         on;                                  
 
        ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;                
        ssl_ciphers          kEECDH+AES128:kEECDH:kEDH:-3DES:kRSA+AES128:kEDH+3DES:DES-CBC3-SHA:!RC4:!aNULL:!eNULL:!MD5:!EXPORT:!LOW:!SEED:!CAMELLIA:!IDEA:!PSK:!SRP:!SS
        ssl_prefer_server_ciphers on;
 
        location   /            { }
        location = /robots.txt  { access_log off; log_not_found off; }
        location = /favicon.ico { access_log off; log_not_found off; }
        location ~ /\.          { access_log off; log_not_found off; deny all; }
        location ~ ~$           { access_log off; log_not_found off; deny all; }
 
        location ~ \.(gif|png|ico|jpg)$ {
                expires 30d;
        }
 
        location ~ \.php$ {
                try_files $uri =404;
                include fastcgi_params;
                fastcgi_pass   unix:/run/php-fpm.mail.socket;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                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_intercept_errors        on;
                fastcgi_ignore_client_abort     off;
                fastcgi_connect_timeout         60;
                fastcgi_send_timeout            180;
                fastcgi_read_timeout            180;
                fastcgi_buffer_size             128k;
                fastcgi_buffers                 4    256k;
                fastcgi_busy_buffers_size       256k;
                fastcgi_temp_file_write_size    256k;
        }
 
        location ~ /(data|conf|bin|inc)/ {
                deny all;
        }
 
        location ~ /\.ht {
                deny  all;
        }
}

Интерфейс к rspamd

rspamd предоставляет www-интерфейс, который висит по адресу http://127.0.0.1:11334. С идейной стороны надо настроить nginx, чтобы он пробрасывал запросы на указанный адрес.

Настойка, которая предлагается на просторах официального сайта выглядит так:

        location /rspamd/ {
                proxy_pass http://127.0.0.1:11334/;
                proxy_set_header Host      $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

Проблема в том, что логотип, расположенный по адресу /rspamd/img/rspamd_logo_navbar.png не отображается, и про него пишут 404. Творческое чтение мануала на nginx показывает, что этот эффект вызван секцией location ~ \.(gif|png|ico|jpg)$, т.к.:

nginx вначале ищет среди всех префиксных location’ов, заданных строками, максимально совпадающий. […skip…] Затем nginx проверяет location’ы, заданные регулярными выражениями, в порядке их следования в конфигурационном файле. При первом же совпадении поиск прекращается и nginx использует совпавший location. Если запросу не соответствует ни одно из регулярных выражений, nginx использует максимально совпавший префиксный location, найденный ранее.

Естественное желание заменить location /rspamd/ на что-то в роде location ~ ^/rspamd/ приводит к ругани при рестарте:

mx ~ # /etc/init.d/nginx restart 
 * Checking nginx' configuration ...
nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block in /etc/nginx/sites/mail.somedomain.ru.conf:44
nginx: configuration file /etc/nginx/nginx.conf test failed
nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block in /etc/nginx/sites/mail.somedomain.ru.conf:44
nginx: configuration file /etc/nginx/nginx.conf test failed
 * failed, please correct errors above                                                                                                                            [ !! ]
 * ERROR: nginx failed to stop

Следующей итерацией удаляем конечный слеш в директиве proxy_pass, что позволяет перезапустить nginx, но выдаёт 404ю ошибку при любом обращении к адресам /rspamd/… На этом месте я надолго застрял, пока не прочитал описание проблемы и способ решения:

URI запроса передаётся на сервер так:
  • Если директива proxy_pass указана с URI, то при передаче запроса серверу часть нормализованного URI запроса, соответствующая location, заменяется на URI, указанный в директиве:
location /name/ {
    proxy_pass http://127.0.0.1/remote/;
}
  • Если директива proxy_pass указана без URI, то при обработке первоначального запроса на сервер передаётся URI запроса в том же виде, в каком его прислал клиент, а при обработке изменённого URI - нормализованный URI запроса целиком:
location /some/path/ {
    proxy_pass http://127.0.0.1;
}

До версии 1.1.12, если proxy_pass указана без URI, в ряде случаев при изменении URI на сервер мог передаваться URI первоначального запроса вместо изменённого URI.

В ряде случаев часть URI запроса, подлежащую замене, выделить невозможно:

  • Если location задан регулярным выражением.

В этом случае директиву следует указывать без URI.

  • Если внутри проксируемого location с помощью директивы rewrite изменяется URI, и именно с этой конфигурацией будет обрабатываться запрос (break):
location /name/ {
    rewrite    /name/([^/]+) /users?name=$1 break;
    proxy_pass http://127.0.0.1;
}

В этом случае URI, указанный в директиве, игнорируется, и на сервер передаётся изменённый URI запроса целиком.

Итого, правильный конфиг будет таким:

        location ~ ^/rspamd/ {
                rewrite    /rspamd(.*) /$1 break;
                proxy_pass http://127.0.0.1:11334;
                proxy_set_header Host      $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

CGI-скрипты

nginx не умеет работать с классическими CGI-скриптами, только FastCGI. Чтобы скрестить nginx с mailgraph ставим дополнительный софт:

emerge -avt www-misc/fcgiwrap
emerge -avt www-servers/spawn-fcgi

Первая софтинка понимает FastCGI-протокол и умеет из под себя запускать классические CGI-скрипты. Вторая – запускатор первой.

cd /etc/init.d/
ln -s spawn-fcgi spawn-fcgi.mail
cp /etc/conf.d/spawn-fcgi{,.mail}
mcedit /etc/conf.d/spawn-fcgi.mail

Содержимое конфига:

/etc/conf.d/spawn-fcgi.mail
FCGI_SOCKET=/run/fastcgi.mail.socket
FCGI_ADDRESS=
FCGI_PORT=
FCGI_PROGRAM=/usr/sbin/fcgiwrap
FCGI_CHILDREN=1
FCGI_CHROOT=
FCGI_CHDIR=
FCGI_USER=nginx
FCGI_GROUP=nginx
 
FCGI_EXTRA_OPTIONS=" -f "
ALLOWED_ENV="PATH DOCUMENT_ROOT SCRIPT_NAME SCRIPT_FILENAME"

Теперь правим конфиг nginx'а и добавляем новый локейшен (уже правильная версия). spawn-fcgi при запуске потомков каждому из них создаёт отдельный сокет. Имя сокета создаётся на основе параметра FCGI_SOCKET, к которому дописывается суффикс «-N», где N – номер потомка. Такм образом, fastcgi_pass должен сожержать значение unix:/run/fastcgi.mail.socket-1

        location ~ \.cgi$ {
                root           /var/www/mail.somedomain.ru;
#                error_log   /var/log/nginx/mail.somedomain.ru-debug debug;
 
                include        fastcgi_params;
                fastcgi_pass   unix:/run/fastcgi.mail.socket-1 ;
                fastcgi_param  DOCUMENT_ROOT    $document_root;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                fastcgi_param  SCRIPT_NAME      $fastcgi_script_name;
 
                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  SCRIPT_NAME        $fastcgi_script_name;
                fastcgi_param  REQUEST_URI        $request_uri;
                fastcgi_param  DOCUMENT_URI       $document_uri;
                fastcgi_param  DOCUMENT_ROOT      $document_root;
                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;
                fastcgi_param  REMOTE_USER        $remote_user;
        }
sys/миграция_на_nginx_с_apache-2.4_или_путь_по_граблям.txt · Последнее изменение: 2017-03-27 00:16
GNU Free Documentation License 1.3 Если не указано иное, содержимое этой вики предоставляется на условиях следующей лицензии: GNU Free Documentation License 1.3