- Автор темы
- Администратор
- Модер.
- #1
FreeBSD 15 как веб-хостинг: от установки до первого сайта с SSL
FreeBSD давно и заслуженно используется для веб-хостинга. Её выбирают за ZFS со снапшотами, jail-ы, стабильность и предсказуемые обновления. Ветка 15 принесла заметные изменения, поэтому даже опытным админам стоит разобраться, что поменялось.
В этой статье соберём полноценный хостинг-сервер на FreeBSD 15.1:
- nginx + PHP-FPM с отдельным пулом и пользователем для каждого сайта;
- MySQL на оптимально настроенном ZFS-датасете;
- бесплатные SSL-сертификаты Let's Encrypt с автопродлением;
- скрипт добавления нового сайта одной командой;
- файрвол, защита от брутфорса, бэкапы и обновления.
0. Что нового во FreeBSD 15 для админа
| Изменение | Что это значит на практике |
|---|---|
| pkgbase | Базовую систему можно обновлять через pkg, как обычные пакеты. В 15.x это опция (технологическое превью), в 16 должна стать единственным способом |
| Срок поддержки | Ветка поддерживается 4 года вместо 5. Минорные релизы нужно обновлять вовремя: 15.0 уже снята с поддержки, актуальна 15.1 |
| blacklistd → blocklistd | Демон защиты от брутфорса переименован, поменялись имена в конфигах |
| Репозиторий пакетов | Называется FreeBSD-ports (раньше просто FreeBSD), рядом появился FreeBSD-base |
Главное правило 15-й ветки: система управляется либо через freebsd-update, либо через pkgbase. Никогда не смешивайте два способа на одном сервере, иначе получите сломанную базовую систему.
1. Установка
При установке с ISO выберите:
- Файловая система: Auto (ZFS). Для хостинга это обязательно: снапшоты, сжатие, загрузочные окружения.
- Способ установки базы:
- Сервисы: sshd, ntpd, dumpdev.
- Hardening: отметьте все пункты, кроме тех, что вам точно мешают.
Дальше в статье команды обновления приведены для обоих вариантов.
2. Первичная настройка
Обновление системы
Классический вариант:
Код:
freebsd-update fetch install
pkgbase:
Код:
pkg update && pkg upgrade -r FreeBSD-base
Свежие пакеты вместо квартальных
По умолчанию pkg использует ветку
quarterly, которая обновляется раз в квартал (плюс исправления безопасности). Для веб-сервера часто нужны свежие версии PHP и nginx, поэтому переключимся на latest:
Код:
mkdir -p /usr/local/etc/pkg/repos
cat > /usr/local/etc/pkg/repos/FreeBSD.conf << 'EOF'
FreeBSD-ports: {
url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest"
}
EOF
pkg update -f
Если вам важнее стабильность, чем свежесть, этот шаг можно пропустить: quarterly тоже получает исправления безопасности.
Администратор и SSH по ключу
Код:
pw useradd admin -m -G wheel -s /bin/sh
passwd admin
mkdir -p /home/admin/.ssh
# вставьте свой публичный ключ:
ee /home/admin/.ssh/authorized_keys
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh && chmod 600 /home/admin/.ssh/authorized_keys
pkg install -y sudo
echo '%wheel ALL=(ALL:ALL) ALL' > /usr/local/etc/sudoers.d/wheel
Проверьте вход по ключу в новом окне, и только потом закрывайте пароли в
/etc/ssh/sshd_config:
Код:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
UseBlocklist yes
Код:
service sshd restart
Почта root на свой ящик
FreeBSD каждый день присылает отчёты о безопасности, уязвимых пакетах и состоянии дисков. Чтобы их получать, в
/etc/mail/aliases:
Код:
root: you@example.com
Код:
newaliases
3. ZFS-датасеты под хостинг
Разнесём сайты и базу по отдельным датасетам: так удобнее снапшотить, ограничивать место и настраивать параметры под нагрузку.
Код:
# Сайты: сжатие zstd отлично работает на PHP/HTML/CSS/JS
zfs create -o mountpoint=/usr/local/www/sites -o compression=zstd zroot/sites
# MySQL: размер записи под страницы InnoDB
zfs create -o mountpoint=/var/db/mysql \
-o recordsize=16k -o compression=lz4 \
-o logbias=throughput -o atime=off zroot/mysql
# Бэкапы дампов
zfs create -o mountpoint=/backup -o compression=zstd zroot/backup
Если хостите чужие сайты, можно ограничить место для отдельного клиента:
Код:
zfs create -o quota=10G zroot/sites/client1.com
4. Установка стека
Код:
pkg install -y nginx \
php84 php84-extensions php84-mysqli php84-pdo_mysql php84-gd \
php84-curl php84-mbstring php84-intl php84-zip php84-exif \
php84-sodium php84-opcache php84-pecl-redis \
mysql84-server valkey
Имена пакетов зависят от актуальных версий. Если какой-то не найден, проверьте черезpkg search php8/pkg search mysql. Вместо MySQL можно поставить MariaDB (pkg search mariadb). Valkey — открытый форк Redis, полностью совместимый по протоколу, подходит для кэша XenForo, WordPress и других движков.
Включаем автозапуск:
Код:
sysrc nginx_enable="YES"
sysrc php_fpm_enable="YES"
sysrc mysql_enable="YES"
sysrc valkey_enable="YES"
5. Тюнинг ядра под веб
FreeBSD умеет кое-что, чего нет в Linux. Например, accept filters: ядро будит nginx только тогда, когда HTTP-запрос пришёл целиком. Это экономит ресурсы и помогает против медленных атак.
/boot/loader.conf:
Код:
accf_http_load="YES"
accf_data_load="YES"
# современный TCP-стек RACK — лучше ведёт себя на плохих каналах
tcp_rack_load="YES"
/etc/sysctl.conf:
Код:
# Использовать RACK по умолчанию
net.inet.tcp.functions_default=rack
# Очередь входящих соединений
kern.ipc.soacceptqueue=4096
# Буферы сокетов
kern.ipc.maxsockbuf=16777216
net.inet.tcp.sendbuf_max=16777216
net.inet.tcp.recvbuf_max=16777216
# Быстрое освобождение соединений
net.inet.tcp.fast_finwait2_recycle=1
net.inet.tcp.finwait2_timeout=5000
# Скрываем закрытые порты от сканеров
net.inet.tcp.blackhole=2
net.inet.udp.blackhole=1
Применяется после перезагрузки (модули) или командой
service sysctl restart (sysctl).6. MySQL
Код:
service mysql-server start
mysql_secure_installation
В
/usr/local/etc/mysql/my.cnf, в секции [mysqld]:
Код:
bind-address = 127.0.0.1
# ZFS уже гарантирует целостность записи — doublewrite не нужен
innodb_doublewrite = 0
innodb_flush_neighbors = 0
# ~50-70% RAM на выделенном сервере, меньше — если на нём ещё и PHP
innodb_buffer_pool_size = 2G
innodb_log_file_size = 512M
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
Код:
service mysql-server restart
7. nginx: базовый конфиг
/usr/local/etc/nginx/nginx.conf:
Код:
user www;
worker_processes auto;
events {
worker_connections 4096;
use kqueue;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
keepalive_timeout 30;
server_tokens off;
client_max_body_size 64m;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
# общие настройки TLS
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 1d;
# заглушка: на IP и неизвестные домены — сброс соединения
server {
listen 80 default_server accept_filter=httpready;
server_name _;
return 444;
}
include /usr/local/etc/nginx/sites/*.conf;
}
Код:
mkdir -p /usr/local/etc/nginx/sites
Обратите внимание на
accept_filter=httpready и use kqueue: это FreeBSD-специфичные оптимизации.8. Изоляция сайтов: пользователь + пул PHP-FPM
Главный принцип хостинга: каждый сайт работает от своего пользователя в своём пуле PHP-FPM. Если на одном сайте взломают плагин, злоумышленник не сможет прочитать файлы и конфиги соседей.
Структура:
Код:
/usr/local/www/sites/example.com/
├── public/ ← корень сайта (DocumentRoot)
├── logs/ ← логи nginx и PHP
└── tmp/ ← сессии и временные файлы PHP
Права:
- владелец — пользователь сайта, PHP работает от его имени;
- группа — группа сайта, в неё добавлен
www, чтобы nginx мог читать статику; - остальные — нет доступа (
750).
Стандартный пул
www нам не нужен, отключаем его:
Код:
mv /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled
cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini
9. Скрипт добавления сайта
Вся рутина — в одном скрипте. Создайте
/usr/local/sbin/newsite:
Код:
#!/bin/sh
# Использование: newsite example.com
set -e
DOMAIN="$1"
[ -z "$DOMAIN" ] && { echo "Usage: $0 domain.com"; exit 1; }
USER=$(echo "$DOMAIN" | tr -c 'a-zA-Z0-9\n' '_' | cut -c1-16)
ROOT="/usr/local/www/sites/$DOMAIN"
POOL="/usr/local/etc/php-fpm.d/$DOMAIN.conf"
VHOST="/usr/local/etc/nginx/sites/$DOMAIN.conf"
SOCK="/var/run/php-fpm-$DOMAIN.sock"
# 1. Пользователь без shell
pw useradd "$USER" -d "$ROOT" -s /usr/sbin/nologin -c "site $DOMAIN"
pw groupmod "$USER" -m www
# 2. Каталоги (отдельный ZFS-датасет)
zfs create "zroot/sites/$DOMAIN"
mkdir -p "$ROOT/public" "$ROOT/logs" "$ROOT/tmp"
echo "<?php echo 'It works: $DOMAIN';" > "$ROOT/public/index.php"
chown -R "$USER:$USER" "$ROOT"
chmod 750 "$ROOT" "$ROOT/public"
chmod 770 "$ROOT/logs"
chmod 700 "$ROOT/tmp"
# 3. Пул PHP-FPM
cat > "$POOL" << EOF
[$DOMAIN]
user = $USER
group = $USER
listen = $SOCK
listen.owner = www
listen.group = www
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
pm.max_requests = 500
php_admin_value[open_basedir] = $ROOT/public:$ROOT/tmp
php_admin_value[upload_tmp_dir] = $ROOT/tmp
php_admin_value[session.save_path] = $ROOT/tmp
php_admin_value[sys_temp_dir] = $ROOT/tmp
php_admin_value[error_log] = $ROOT/logs/php-error.log
php_admin_flag[log_errors] = on
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
EOF
# 4. Виртуальный хост nginx (пока только HTTP — для получения сертификата)
cat > "$VHOST" << EOF
server {
listen 80 accept_filter=httpready;
server_name $DOMAIN www.$DOMAIN;
root $ROOT/public;
index index.php index.html;
access_log $ROOT/logs/access.log;
error_log $ROOT/logs/error.log;
location /.well-known/acme-challenge/ { allow all; }
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php\$ {
try_files \$uri =404;
fastcgi_pass unix:$SOCK;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\. { deny all; }
}
EOF
# 5. Применяем
nginx -t
service php_fpm reload
service nginx reload
echo "Сайт $DOMAIN создан. Пользователь: $USER"
echo "Следующий шаг: certbot certonly --webroot -w $ROOT/public -d $DOMAIN -d www.$DOMAIN"
Код:
chmod 700 /usr/local/sbin/newsite
service php_fpm start
service nginx start
newsite example.com
База данных для сайта
Код:
mysql -u root -p
CREATE DATABASE example_com;
CREATE USER 'example_com'@'localhost' IDENTIFIED BY 'сложный_пароль';
GRANT ALL PRIVILEGES ON example_com.* TO 'example_com'@'localhost';
FLUSH PRIVILEGES;
10. SSL от Let's Encrypt
Код:
pkg search certbot # узнать точное имя пакета
pkg install -y py311-certbot # подставьте имя из поиска
Получаем сертификат (DNS домена уже должен указывать на сервер):
Код:
certbot certonly --webroot \
-w /usr/local/www/sites/example.com/public \
-d example.com -d www.example.com \
--email you@example.com --agree-tos
Сертификаты лежат в
/usr/local/etc/letsencrypt/live/example.com/.Дописываем в
/usr/local/etc/nginx/sites/example.com.conf HTTPS-блок, а в HTTP-блоке включаем редирект:
Код:
server {
listen 80 accept_filter=httpready;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /usr/local/www/sites/example.com/public;
}
location / { return 301 https://example.com$request_uri; }
}
server {
listen 443 ssl accept_filter=dataready;
http2 on;
server_name example.com www.example.com;
ssl_certificate /usr/local/etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
root /usr/local/www/sites/example.com/public;
index index.php index.html;
access_log /usr/local/www/sites/example.com/logs/access.log;
error_log /usr/local/www/sites/example.com/logs/error.log;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm-example.com.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2?)$ {
expires 30d;
access_log off;
}
location ~ /\. { deny all; }
}
Код:
nginx -t && service nginx reload
Автопродление: certbot ставит скрипт в periodic, его нужно только включить в
/etc/periodic.conf:
Код:
weekly_certbot_enable="YES"
weekly_certbot_post_hook="service nginx reload"
Проверить продление вхолостую:
Код:
certbot renew --dry-run
11. PHP: OPcache и безопасность
В
/usr/local/etc/php.ini:
Код:
expose_php = Off
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 60
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 50000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
Код:
service php_fpm restart
12. Файрвол и blocklistd
Минимальный
/etc/pf.conf для хостинга:
Код:
ext_if = "vtnet0"
table <blocked> persist
set skip on lo0
set block-policy drop
scrub in on $ext_if all fragment reassemble
block log all
antispoof quick for $ext_if
anchor "blocklistd/*" in on $ext_if
block drop in quick on $ext_if from <blocked>
pass out quick on $ext_if keep state
pass in on $ext_if proto tcp to ($ext_if) port 22 flags S/SA keep state \
(max-src-conn 10, max-src-conn-rate 5/30, overload <blocked> flush global)
pass in on $ext_if proto tcp to ($ext_if) port { 80, 443 } flags S/SA keep state
pass in on $ext_if proto udp to ($ext_if) port 443 keep state
pass in on $ext_if inet proto icmp icmp-type { echoreq, unreach } keep state
pass in on $ext_if inet6 proto ipv6-icmp keep state
Код:
sysrc pf_enable="YES" pflog_enable="YES"
sysrc blocklistd_enable="YES" blocklistd_flags="-r"
pfctl -nf /etc/pf.conf
nohup sh -c 'sleep 300 && pfctl -d' >/dev/null 2>&1 &
service pf start && service pflog start && service blocklistd start
# новый SSH работает? Отменяем таймер:
pkill -f "sleep 300"
Конфиг правил бана находится в
/etc/blocklistd.conf, просмотр банов — blocklistctl dump -b.Подробный разбор pf (GeoIP, Cloudflare, jail-ы, шейпинг) — в нашей серии статей «FreeBSD + pf». Правила оттуда работают и на 15-й ветке, нужно лишь заменить blacklistd на blocklistd.
13. Бэкапы
Снапшоты ZFS
Снапшот создаётся мгновенно и почти не занимает места. Это лучшая защита от «удалил не то» и от вирусов-шифровальщиков на сайтах.
Код:
pkg install -y sanoid
/usr/local/etc/sanoid/sanoid.conf:
Код:
[zroot/sites]
use_template = production
recursive = yes
[zroot/mysql]
use_template = production
[template_production]
hourly = 24
daily = 14
monthly = 3
autosnap = yes
autoprune = yes
В
/etc/crontab:
Код:
*/15 * * * * root /usr/local/bin/sanoid --cron
Восстановить один файл можно прямо из скрытого каталога снапшота:
Код:
ls /usr/local/www/sites/example.com/.zfs/snapshot/
cp /usr/local/www/sites/example.com/.zfs/snapshot/autosnap_..._hourly/public/wp-config.php \
/usr/local/www/sites/example.com/public/
Дампы баз
Снапшот работающей MySQL восстановится, но для надёжности делайте ещё и логические дампы. Скрипт
/usr/local/sbin/dbbackup:
Код:
#!/bin/sh
DIR="/backup/mysql/$(date +%Y-%m-%d)"
mkdir -p "$DIR"
for db in $(mysql -N -e "SHOW DATABASES" | grep -Ev '^(information_schema|performance_schema|sys|mysql)$'); do
mysqldump --single-transaction --routines --triggers "$db" | zstd -q > "$DIR/$db.sql.zst"
done
find /backup/mysql -type d -mtime +14 -exec rm -rf {} +
Пароль root для скрипта положите в
/root/.my.cnf с правами 600:
Код:
[client]
user=root
password=пароль_root_mysql
Запуск каждую ночь:
Код:
30 3 * * * root /usr/local/sbin/dbbackup
Помните: бэкап на том же сервере — это не бэкап. Раз в сутки отправляйте/backupи снапшоты на другой сервер (zfs send | ssh ... zfs recvилиsyncoidиз того же пакета sanoid).
14. Обновления без страха: загрузочные окружения
ZFS позволяет перед обновлением сохранить всю систему целиком и откатиться одной командой, если что-то пошло не так.
Код:
# Перед любым обновлением системы
bectl create before-update-$(date +%Y%m%d)
bectl list
Обновление (классика):
Код:
freebsd-update fetch install
pkg upgrade
Обновление (pkgbase):
Код:
pkg upgrade -r FreeBSD-base
pkg upgrade -r FreeBSD-ports
Если после перезагрузки сервер не работает как надо:
Код:
bectl activate before-update-20261028
shutdown -r now
Или выберите нужное окружение прямо в загрузчике FreeBSD через KVM-консоль.
Сайты и база лежат в отдельных датасетах (zroot/sites,zroot/mysql), которые не входят в загрузочное окружение. Поэтому откат системы не откатит данные сайтов: так и задумано.
15. Ежедневное обслуживание
Код:
pkg audit -F # уязвимости в установленных пакетах
pkg version -vl '<' # что можно обновить
zpool status -x # здоровье дисков
zfs list -o name,used,avail # место по сайтам
sockstat -4l # что слушает порты
top -o res # кто ест память
Ротация логов сайтов. Добавьте в
/etc/newsyslog.conf:
Код:
/usr/local/www/sites/*/logs/*.log 640 14 * @T00 GJ /var/run/nginx.pid 30
Это означает: ежедневная ротация, хранить 14 архивов со сжатием, затем nginx получает сигнал переоткрыть логи. Флаг
G включает поддержку шаблона * в пути.Что дальше
У вас готов хостинг-сервер, на котором:
- каждый сайт изолирован своим пользователем, пулом PHP и ZFS-датасетом;
- новый сайт добавляется одной командой;
- SSL выдаётся бесплатно и продлевается сам;
- данные защищены снапшотами каждые 15 минут;
- неудачное обновление откатывается за минуту.
Для следующего уровня изоляции, когда каждый клиент живёт в своём jail-е, присмотритесь к менеджеру Bastille (
pkg install bastille) или к ручной схеме из нашей статьи «pf + jail-ы».Вопросы и свой опыт пишите в теме!
