psj-notes
~/home ~/series ~/archive

Часть 07. Nginx, PHP-FPM и SSL — веб-сервер и фронтенд

01.01.1970 Серия: Руководство по настройке srv02-Helsinki 'nginx' 'php' 'ssl' 'certbot'

Часть 07. Nginx, PHP-FPM и SSL — веб-сервер и фронтенд

«Дом построен. Теперь нужна дверь, на которой написано «Вход», и табличка с именем хозяина. Nginx будет этой дверью, PHP — прислугой, а Let's Encrypt — позолотой на табличке.» — Воланд


Глава 1. Что такое Nginx, PHP-FPM и зачем нам SSL

Три компонента, которые превращают сервер из пустого ящика в веб-площадку.

Nginx — веб-сервер и обратный прокси. Он принимает HTTP/HTTPS-запросы, отдаёт статические файлы (HTML, CSS, JS, картинки), а динамические запросы (PHP) передаёт PHP-FPM. В нашей архитектуре Nginx слушает:

  • порт 8080 (HTTP) — потому что порт 80 занят sslh, который направляет HTTP к Nginx
  • порт 443 (HTTPS) — напрямую, с SSL-сертификатом

PHP-FPM (FastCGI Process Manager) — менеджер процессов PHP. Nginx не умеет выполнять PHP-скрипты сам — он передаёт их PHP-FPM через Unix-сокет. PHP-FPM держит пул процессов, которые ждут запросов.

Let's Encrypt — бесплатный центр сертификации. Выдаёт SSL-сертификаты для HTTPS. Сертификаты живут 90 дней и автоматически продлеваются.

Бегемот, натирая дверную ручку:
«Nginx — швейцар, который встречает гостей. PHP-FPM — повар на кухне, готовит страницы. SSL — позолоченная табличка, чтобы гости знали: дом надёжный.»


Глава 2. Установка Nginx

Nginx есть в стандартных репозиториях Ubuntu:

sudo apt install -y nginx

Ожидаемый вывод (в конце):

Setting up nginx (1.24.0-2ubuntu7.15) ...

Проверяем версию:

nginx -v

Ожидаемый вывод:

nginx version: nginx/1.24.0 (Ubuntu)

Nginx установлен, но пока не настроен. Проверим, что он не запущен (будем настраивать вручную перед запуском):

sudo systemctl status nginx --no-pager | head -5

Если статус inactive (dead) — всё в порядке. Если active — временно остановим:

sudo systemctl stop nginx

Коровьев, примеряя швейцарскую ливрею:
«Швейцар нанят. Ливрея сшита. Но пока он не знает, в какой подъезд встать. Дайте инструкцию — и он займёт пост.»


Глава 3. Установка PHP 8.3-FPM

На сервере используется PHP 8.3 — последняя стабильная ветка на момент настройки.

Устанавливаем PHP-FPM и необходимые модули:

sudo apt install -y php8.3-fpm php8.3-cli php8.3-common php8.3-mbstring php8.3-curl php8.3-xml php8.3-zip php8.3-intl php8.3-yaml

Ожидаемый вывод (в конце):

Setting up php8.3-fpm (8.3.6-0ubuntu0.24.04.10) ...

Проверяем версию CLI:

php -v

Ожидаемый вывод:

PHP 8.3.6 (cli) (built: Jul 16 2026 18:30:41) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies

Проверяем, что PHP-FPM запущен:

sudo systemctl status php8.3-fpm --no-pager | head -5

Ожидаемый вывод:

● php8.3-fpm.service - The PHP FastCGI Process Manager
     Loaded: loaded (/lib/systemd/system/php8.3-fpm.service; enabled; ...)
     Active: active (running) since ...

Настраиваем пул PHP-FPM под ресурсы сервера (1 vCPU, 2 ГБ RAM). Редактируем конфиг пула:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Находим и меняем параметры:

pm = static
pm.max_children = 3
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Пояснение:

  • pm = static — фиксированное количество процессов PHP. Для сервера с 2 ГБ RAM и одним сайтом статичный пул надёжнее динамического.
  • pm.max_children = 3 — три PHP-процесса. Каждый процесс потребляет ~30-50 МБ RAM. Итого ~120-150 МБ на весь PHP, остальное — Nginx и система.

Перезапускаем PHP-FPM:

sudo systemctl restart php8.3-fpm

Проверяем, что сокет создался:

ls -la /run/php/php8.3-fpm.sock

Ожидаемый вывод:

srw-rw---- 1 www-data www-data 0 Jul 26 08:07 /run/php/php8.3-fpm.sock

Воланд, открывая кухню:
«Три повара на кухне. Больше не надо — помещение маленькое. Каждый знает своё дело: принял запрос, приготовил страницу, отдал. Устал — отдохнул. Трое справляются.»


Глава 4. Настройка HTTP-сервера (порт 8080)

Вспомним архитектуру: порт 80 занят sslh (часть 05). sslh анализирует трафик: если это SSH — отправляет на порт 22022, если HTTP — на порт 8080. Значит, Nginx должен слушать порт 8080 для HTTP-запросов.

Создаём директорию для Let's Encrypt challenge (проверка домена):

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge

Создаём конфигурационный файл основного сайта:

sudo nano /etc/nginx/sites-available/psj-notes-ru

Начинаем с HTTP-сервера (порт 8080). Это временная конфигурация — её задача только в том, чтобы certbot мог подтвердить владение доменом через ACME challenge. После получения сертификата мы заменим её на финальную.

Для ACME-проверки нужно, чтобы Nginx отдавал файлы из /var/www/letsencrypt/.well-known/acme-challenge/ по любому из доменов. Создаём конфиг для foto.psj-notes.ru (он будет обслуживать challenge для всех доменов):

sudo nano /etc/nginx/sites-available/psj-notes-ru

Содержимое (временное):

server {
    server_name psj-notes.ru www.psj-notes.ru foto.psj-notes.ru photo.psj-notes.ru;

    location /.well-known/acme-challenge/ {
        alias /var/www/letsencrypt/.well-known/acme-challenge/;
    }

    location / {
        return 301 https://$host$request_uri;
    }

    listen 127.0.0.1:8080;
}

Пояснение:

  • server_name — перечисляем все домены, для которых будем получать сертификаты
  • location /.well-known/acme-challenge/ — открытая директория для проверки домена Let's Encrypt
  • location / — всё остальное отправляем на HTTPS через код 301 (Moved Permanently)
  • listen 127.0.0.1:8080 — слушаем только на локальном интерфейсе, порт 8080. Внешний мир видит только порт 80 (sslh), который направляет сюда.

Включаем сайт (создаём симлинк в sites-enabled):

sudo ln -s /etc/nginx/sites-available/psj-notes-ru /etc/nginx/sites-enabled/

Важно: После получения сертификатов мы вернёмся к этому конфигу и приведём его к окончательному виду (глава 6). Сейчас — только чтобы certbot мог проверить домены.

Коровьев, развешивая таблички:
«Парадная дверь (80) — за sslh. Чёрный ход (8080) — наш, для Nginx. Если гость пришёл по адресу psj-notes.ru, его встречает швейцар и ведёт через 8080 к HTTPS. Вежливо, но настойчиво.»


Глава 5. Let's Encrypt SSL

Устанавливаем Certbot — клиент Let's Encrypt:

sudo apt install -y certbot python3-certbot-nginx

Ожидаемый вывод:

Setting up certbot (2.9.0-1) ...

Получаем сертификаты для всех доменов. Certbot автоматически проверит владение доменом (через ACME-запрос к /.well-known/acme-challenge/ на нашем сервере), создаст сертификат и пропишет SSL в конфиг Nginx.

Начинаем с основного домена:

sudo certbot --nginx -d psj-notes.ru -d www.psj-notes.ru

Ожидаемый вывод (интерактивно, потребуется ответить на вопросы):

Saving debug log to /var/log/letsencrypt/letsencrypt.log

Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: psj-notes.ru
2: www.psj-notes.ru
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 

Нажимаем Enter (выбрать все). Certbot выполнит проверку ACME, получит сертификат и изменит конфиг, добавив HTTPS-блок.

Ожидаемый вывод:

Successfully received certificate.
Certificate is saved at /etc/letsencrypt/live/psj-notes.ru/fullchain.pem
Key is saved at /etc/letsencrypt/live/psj-notes.ru/privkey.pem
This certificate expires on 2026-10-24.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for psj-notes.ru to /etc/nginx/sites-enabled/psj-notes-ru
Successfully deployed certificate for www.psj-notes.ru to /etc/nginx/sites-enabled/psj-notes-ru
Congratulations! You have successfully enabled HTTPS on https://psj-notes.ru and https://www.psj-notes.ru

Теперь получаем сертификат для фото-домена:

sudo certbot --nginx -d foto.psj-notes.ru -d photo.psj-notes.ru

Аналогично выбираем все домены, нажимаем Enter.

Ожидаемый вывод:

Successfully received certificate.
Certificate is saved at /etc/letsencrypt/live/foto.psj-notes.ru/fullchain.pem
Key is saved at /etc/letsencrypt/live/foto.psj-notes.ru/privkey.pem

Deploying certificate
Successfully deployed certificate for foto.psj-notes.ru to /etc/nginx/sites-enabled/psj-notes-ru
Successfully deployed certificate for photo.psj-notes.ru to /etc/nginx/sites-enabled/psj-notes-ru
Congratulations! You have successfully enabled HTTPS on https://foto.psj-notes.ru and https://photo.psj-notes.ru

Теперь у нас есть сертификаты для всех доменов. После этого мы вернёмся к конфигам и приведём их к финальному виду.

Сертификаты хранятся в /etc/letsencrypt/live/psj-notes.ru/:

  • fullchain.pem — полная цепочка сертификатов (публичный ключ)
  • privkey.pem — приватный ключ (никому не показывать!)

Проверяем настройки SSL, которые Certbot добавил в конфиг:

sudo nano /etc/letsencrypt/options-ssl-nginx.conf

Содержимое:

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

Проверяем автообновление сертификатов. Certbot устанавливает systemd-таймер:

sudo systemctl status certbot.timer --no-pager | head -10

Ожидаемый вывод:

● certbot.timer - Run certbot twice daily
     Loaded: loaded (/usr/lib/systemd/system/certbot.timer; enabled; ...)
     Active: active (waiting) since ...

  ...

     Trigger: Mon 2026-07-27 07:37:58 EEST

Таймер запускает certbot renew дважды в сутки (00:00 и 12:00) со случайной задержкой до 12 часов. Если до окончания срока сертификата осталось меньше 30 дней — certbot продлит его автоматически.

Воланд, вешая табличку:
«Табличка позолочена, имя выгравировано. Дом теперь не просто «psj-notes.ru», а «https://psj-notes.ru». Гости видят замок в адресной строке — знак качества. Certbot будет проверять позолоту каждые 12 часов.»


Глава 6. HTTPS-конфигурация psj-notes.ru

После получения сертификатов Certbot модифицировал временный конфиг, добавив HTTPS-блок. Теперь заменяем его на финальный — разделяем psj-notes.ru и foto.psj-notes.ru по разным файлам, расставляем защиты.

Редактируем /etc/nginx/sites-available/psj-notes-ru:

sudo nano /etc/nginx/sites-available/psj-notes-ru

Полное содержимое (всё заменяем):

server {
    server_name psj-notes.ru www.psj-notes.ru;

    root /home/sergey/projects/www/public;

    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /admin {
        alias /home/sergey/projects/www/admin;
        index index.php login.php;
        try_files $uri $uri/ /admin/index.php?$query_string;

        location ~ \.php$ {
            include snippets/fastcgi-php.conf;
            fastcgi_pass unix:/run/php/php8.3-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $request_filename;
        }
    }

    location /admin/lib {
        internal;
    }

    location ~ ^/data/ {
        internal;
    }

    location ~ ^/templates/ {
        internal;
    }

    location ~ ^/scripts/ {
        internal;
    }

    location ~ ^/vendor/ {
        internal;
    }

    location ~ /\. {
        deny all;
    }

    listen [::]:443 ssl ipv6only=on;
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/psj-notes.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/psj-notes.ru/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
    if ($host = www.psj-notes.ru) {
        return 301 https://$host$request_uri;
    }

    if ($host = psj-notes.ru) {
        return 301 https://$host$request_uri;
    }

    listen 127.0.0.1:8080;

    server_name psj-notes.ru www.psj-notes.ru;
    return 404;
}

Пояснение по блокам:

HTTP-блок (порт 8080):

  • Проверяет $host — если это psj-notes.ru или www.psj-notes.ru — перенаправляет на HTTPS
  • Во всех остальных случаях (боты, сканеры) — возвращает 404
  • if ($host = ...) return 301 — certbot генерирует такую конструкцию для редиректа

HTTPS-блок (порт 443):

  • root /home/sergey/projects/www/public — корневая директория сайта (статический HTML)
  • location / — отдаёт статические файлы (index.html, CSS, JS)
  • location /admin — alias на папку админки; PHP-файлы обрабатываются через PHP-FPM
  • location ~ \.php$ — внутри /admin передаёт PHP-запросы в сокет php8.3-fpm.sock
  • location /admin/lib, data/, templates/, scripts/, vendor/internal; — эти директории доступны только через внутренние редиректы Nginx (не напрямую из браузера)
  • location ~ /\.deny all; — скрытые файлы (.gitignore, .env и т.д.) недоступны извне
  • ssl_certificate и ssl_certificate_key — пути к сертификатам

Коровьев, расставляя таблички «Входа нет»:
«Парадная дверь — для всех. Библиотека (admin) — только для своих, через PHP-швейцара. Кладовки (data, lib, vendor) — вообще заперты. А файлы, начинающиеся с точки, — под семью замками. Ни один гость не сунется туда, куда не надо.»


Глава 7. Виртуальный хост foto.psj-notes.ru (прокси на домашний Immich)

Второй домен — foto.psj-notes.ru — прокси на домашний сервер, где работает Immich (фото-галерея). Nginx просто передаёт запросы на домашний сервер через Tailscale.

Сертификаты для этого домена уже получены (глава 5). Переносим ACME-директорию сюда и создаём отдельный конфиг:

sudo nano /etc/nginx/sites-available/immich

Полное содержимое:

server {
    server_name foto.psj-notes.ru photo.psj-notes.ru;

    location /.well-known/acme-challenge/ {
        alias /var/www/letsencrypt/.well-known/acme-challenge/;
    }

    location / {
        return 301 https://$host$request_uri;
    }

    listen 127.0.0.1:8080;
}

server {
    server_name foto.psj-notes.ru photo.psj-notes.ru;

    location / {
        proxy_pass http://100.113.180.60:2283;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        client_max_body_size 0;
    }

    listen 443 ssl;
    listen [::]:443 ssl;
    ssl_certificate /etc/letsencrypt/live/foto.psj-notes.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/foto.psj-notes.ru/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

Пояснение параметров:

HTTP-блок (порт 8080):

  • location /.well-known/acme-challenge/ — открытая директория для проверки домена при продлении сертификата
  • location / — всё остальное отправляем на HTTPS

HTTPS-блок (порт 443, прокси):

  • proxy_pass http://100.113.180.60:2283 — передаём запросы на домашний сервер (через Tailscale), порт 2283 (Immich)
  • proxy_http_version 1.1 — для WebSocket-соединений (нужно Immich для загрузки фото)
  • proxy_set_header Host $host — сохраняем оригинальный заголовок Host
  • proxy_set_header Upgrade $http_upgrade и Connection "upgrade" — поддержка WebSocket
  • proxy_read_timeout 300s — таймаут чтения 5 минут (большие фото)
  • client_max_body_size 0 — безлимитный размер тела запроса (оригинальные фото могут быть большими)

Включаем сайт:

sudo ln -s /etc/nginx/sites-available/immich /etc/nginx/sites-enabled/

Бегемот, прокладывая трубу:
«foto.psj-notes.ru — это витрина, а сам магазин (Immich) стоит дома, в Подмосковье. Nginx на VDS — просто витрина: принимает заказ, передаёт его домой через Tailscale-трубу и отдаёт результат гостю. Гость видит только витрину.»


Глава 8. Проверка конфигурации и запуск

Перед запуском проверяем синтаксис всех конфигов:

sudo nginx -t

Ожидаемый вывод:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Запускаем Nginx и добавляем в автозагрузку:

sudo systemctl start nginx && sudo systemctl enable nginx

Ожидаемый вывод:

Synchronizing state of nginx.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable nginx

Проверяем статус всех трёх сервисов:

sudo systemctl status nginx --no-pager | head -5
sudo systemctl status php8.3-fpm --no-pager | head -5
sudo systemctl status certbot.timer --no-pager | head -5

Все три должны быть active (running) или active (waiting) для таймера.

Проверяем, что сайт отвечает через sslh (HTTP → 8080):

curl -I http://localhost:80

Ожидаемый вывод (редирект на HTTPS):

HTTP/1.1 301 Moved Permanently
Server: nginx/1.24.0 (Ubuntu)
Location: https://psj-notes.ru/

Проверяем HTTPS напрямую:

curl -Ik https://localhost:443

Ожидаемый вывод:

HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html

Если сайт уже наполнен контентом — curl вернёт его содержимое.

Проверяем, что административная панель отвечает:

curl -Ik https://localhost:443/admin/login.php

Ожидаемый вывод:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

Проверяем, что защищённые директории не доступны напрямую:

curl -I https://localhost:443/data/somefile.txt

Ожидаемый вывод:

HTTP/1.1 404 Not Found

Или:

HTTP/1.1 444 No Response

Воланд, принимая работу:
«Швейцар на месте, повара готовы, табличка блестит. Парадная дверь (80) ведёт в прихожую sslh, оттуда — в зал Nginx на 8080. Почётный вход (443) — сразу в гостиную, с позолотой Let's Encrypt. Админка — через PHP-буфет. Можно открываться.»


Финал части: Резюме Воланда

Параметр Статус
Nginx 1.24.0 установлен
Nginx слушает порт 8080 (HTTP через sslh)
Nginx слушает порт 443 (HTTPS напрямую)
PHP 8.3-FPM установлен
Пул PHP-FPM: static, max_children=3
Let's Encrypt SSL: psj-notes.ru
Let's Encrypt SSL: foto.psj-notes.ru
Автообновление сертификатов (systemd timer)
HTTP → HTTPS редирект
Прокси foto.psj-notes.ru → домашний Immich
Защита внутренних директорий
Защита скрытых файлов (.*)

Воланд подводит итог седьмой части:
«Фасад готов. Nginx встречает гостей у входа, PHP-FPM готовит страницы на кухне, Let's Encrypt ручается за честность дома. Два адреса — psj-notes.ru для блога и foto.psj-notes.ru для фотографий. Один живёт здесь, второй — через туннель дома. Но гость видит только красивые страницы. Пора наполнять дом контентом!»

Комментарии

Комментарии пока не подключены.