Часть 07. Nginx, PHP-FPM и SSL — веб-сервер и фронтенд
Часть 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 Encryptlocation /— всё остальное отправляем на 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-FPMlocation ~ \.php$— внутри/adminпередаёт PHP-запросы в сокетphp8.3-fpm.socklocation /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— сохраняем оригинальный заголовок Hostproxy_set_header Upgrade $http_upgradeиConnection "upgrade"— поддержка WebSocketproxy_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 для фотографий. Один живёт здесь, второй — через туннель дома. Но гость видит только красивые страницы. Пора наполнять дом контентом!»
Комментарии
Комментарии пока не подключены.