Введение
Эта инструкция — пошаговый воспроизводимый шаблон настройки чистого VPS (Ubuntu) для одновременной работы нескольких изолированных проектов.
Суть архитектуры:
- Наружу открыты только порты
80и443. - Единственная точка входа — легковесный реверс-прокси Caddy, который берет на себя маршрутизацию и автоматический выпуск SSL-сертификатов.
- Проекты изолированы в своих
docker-composeфайлах и общаются с Caddy через единую виртуальную сетьweb_gateway. Базы данных и служебные сервисы наружу не светятся. - Все операции выполняются под выделенным пользователем
deployс правами1000:1000.
Следуя шагам ниже, вы получите готовую к продакшену инфраструктуру, куда новые сайты добавляются за пару минут.
1. Пользователь deploy и директории
Запуск веб-контейнеров и сервисов напрямую под суперпользователем root небезопасен. Поэтому под root мы выполняем только первичную инициализацию — создаём системного пользователя deploy, добавляем его в группы sudo и docker и выделяем ему рабочие директории. Вся дальнейшая настройка и деплой сайтов выполняются строго от имени deploy.
1.1. Создание пользователя
Под пользователем root:
adduser deploy
usermod -aG sudo deploy
usermod -aG docker deploy
1.2. Настройка SSH-доступа и безопасности
Чтобы подключаться к серверу напрямую под пользователем deploy без пароля и защитить систему от брутфорс-атак, настроим вход по ключу и отключим небезопасные методы аутентификации.
1. Добавление публичного ключа
На сервере под пользователем root подготовьте директорию и файл авторизованных ключей:
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys
Вставьте публичный ключ вашего локального компьютера (содержимое локального файла ~/.ssh/id_ed25519.pub). Если у вас ещё не создан SSH-ключ, обратитесь к подробному руководству Создание SSH-ключей и их настройка.
Задайте строгие права доступа и назначьте пользователя deploy владельцем:
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
2. Проверка входа по ключу
В новом окне терминала на вашем локальном компьютере проверьте прямое подключение:
ssh deploy@<ip_сервера>
Убедитесь, что вход прошёл успешно без запроса пароля, а команда sudo whoami отвечает root.
3. Отключение паролей и входа под root
После подтверждения успешного входа под deploy заблокируйте вход по паролю и прямой вход суперпользователя root. Создайте файл конфигурации:
sudo nano /etc/ssh/sshd_config.d/99-security.conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Проверьте конфигурацию на ошибки синтаксиса и примените настройки:
sudo sshd -t
sudo systemctl reload ssh
Безопасность SSH
4. Быстрый доступ через локальный SSH config
На своем локальном компьютере добавьте запись в ~/.ssh/config:
Host my-vps
HostName <ip_сервера>
User deploy
IdentityFile ~/.ssh/id_ed25519
Теперь подключаться к серверу можно короткой командой: ssh my-vps.
1.3. Создание структуры папок
Для порядка и изоляции разделим инфраструктуру прокси и сами сайты на две независимые папки:
/var/www/proxy— директория для центрального реверс-прокси Caddy (здесь размещаются егоdocker-compose.ymlи конфигурацияCaddyfile);/var/www/apps— каталог для проектов (каждый сайт или сервис живёт в своей изолированной подпапке).
Под пользователем deploy создайте структуру каталогов и назначьте его владельцем всей ветки /var/www:
sudo mkdir -p /var/www/proxy /var/www/apps
sudo chown -R deploy:deploy /var/www
Передача прав (chown) гарантирует, что все последующие операции — создание файлов, git clone, сборка образов и запуск контейнеров — будут выполняться пользователем deploy напрямую, без вызова sudo и без конфликта прав доступа к логам и кэшу.
2. Центральный реверс-прокси Caddy
2.1. Создание общей Docker-сети
Перед запуском прокси создайте внешнюю сеть-мост web_gateway для связи Caddy с веб-контейнерами проектов:
docker network create web_gateway
2.2. Создание конфигурации Caddy
Перейдите в директорию прокси:
cd /var/www/proxy
nano docker-compose.yml
Содержимое docker-compose.yml:
services:
caddy:
image: caddy:2-alpine
container_name: caddy_proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web_gateway
networks:
web_gateway:
external: true
volumes:
caddy_data:
caddy_config:
Создайте Caddyfile:
nano /var/www/proxy/Caddyfile
{
email admin@example.com
}
2.3. Запуск и перезагрузка
Запустите Caddy:
docker compose -f /var/www/proxy/docker-compose.yml up -d
Команда перезагрузки
3. SSH-доступ к GitHub
Чтобы сервер мог клонировать приватные репозитории и обновлять код проектов командой git pull при деплое (как вручную, так и автоматически через пайплайны GitHub Actions), настроим сервисный SSH-ключ. Это избавит от необходимости ввода логина/пароля при обращениях к Git и защитит основной аккаунт разработчика.
Под пользователем deploy сгенерируйте отдельный ключ Ed25519 без парольной фразы:
ssh-keygen -t ed25519 -C "deploy" -f ~/.ssh/github_deploy
Создайте ~/.ssh/config для автоматического использования этого ключа:
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_deploy
IdentitiesOnly yes
Выставьте строгие права доступа:
chmod 600 ~/.ssh/config ~/.ssh/github_deploy
chmod 644 ~/.ssh/github_deploy.pub
cat ~/.ssh/github_deploy.pub
Скопируйте публичный ключ и добавьте его в GitHub -> Settings -> SSH Keys (или в настройки конкретного репозитория как Deploy Key), после чего проверьте соединение:
ssh -T git@github.com
Ответ с приветствием Hi ...! You've successfully authenticated... подтверждает готовность сервера к загрузке исходного кода по SSH.
4. Создание и запуск нового сайта
После подготовки сервера добавление нового проекта выполняется по единому трёхшаговому алгоритму.
4.1. Настройка DNS и SSL в Cloudflare
Перед запуском проекта направьте домен на IP-адрес вашего сервера и задайте параметры шифрования в панели Cloudflare:
-
Создание A-записи (DNS → Records):
- Name: имя домена или поддомена (
mysite.example.comили@); - IPv4 address: публичный IP-адрес вашего VPS (
<ip_сервера>); - Proxy status: Proxied (оранжевое облако).
- Name: имя домена или поддомена (
-
Режим шифрования (SSL/TLS → Overview): установите режим Full (strict).
(Режим Flexible вызывает бесконечный редирект 301 между Cloudflare и Caddy). -
HTTPS и перенаправления:
- SSL/TLS → Edge Certificates: включите Always Use HTTPS;
- Rules → Redirect Rules: создайте правило для перенаправления с
www.*на кореньhttps://${1}.
-
Поддомены 4-го уровня (например,
www.app.example.com):
Бесплатный Universal SSL от Cloudflare не покрывает домены глубже 3-го уровня. Для таких записей в DNS выставляйте статус DNS only (серое облако), а вCaddyfileдобавьте редирект:
www.app.example.com {
redir https://app.example.com{uri} permanent
}
4.2. Развертывание контейнера проекта
Создайте директорию приложения в /var/www/apps, добавьте простой тестовый скрипт и запустите контейнер на базе FrankenPHP. Главное требование к docker-compose.yml любого проекта — подключение к внешней сети web_gateway и отсутствие опубликованных наружу портов (ports):
mkdir -p /var/www/apps/my-app
cd /var/www/apps/my-app
echo "<?php phpinfo();" > index.php
nano docker-compose.yml
services:
app:
image: dunglas/frankenphp:php8.3-alpine
container_name: myapp
restart: unless-stopped
volumes:
- .:/app
networks:
- web_gateway
networks:
web_gateway:
external: true
Запустите проект:
docker compose up -d
4.3. Маршрутизация в Caddy и выпуск SSL
Откройте конфигурацию реверс-прокси:
nano /var/www/proxy/Caddyfile
Добавьте блок сайта с указанием имени контейнера приложения и его внутреннего порта:
mysite.example.com {
reverse_proxy myapp:80
}
Примените изменения без остановки Caddy и соседних контейнеров:
docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile
Caddy автоматически получит SSL-сертификат от Let’s Encrypt / ZeroSSL, и сайт сразу станет доступен по HTTPS.
Безопасность и оптимизация сервера
Для стабильной работы продакшена необходимо защитить сервер от переполнения диска, нехватки оперативной памяти и нежелательного сетевого трафика.
Настройка фаервола (UFW)
Закройте все входящие соединения, оставив только порты SSH, HTTP и HTTPS (включая UDP для поддержки HTTP/3):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
Так как контейнеры сайтов не публикуют порты наружу (ports отсутствуют) и общаются с Caddy через изолированную сеть web_gateway, базы данных и служебные порты остаются полностью закрытыми от внешнего мира.
Ротация логов Docker
По умолчанию Docker не ограничивает размер логов контейнеров, что со временем приводит к 100% заполнению диска и аварийной остановке всех сервисов. Ограничьте размер логов глобально в /etc/docker/daemon.json:
sudo nano /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Перезапустите демон Docker для применения настроек:
sudo systemctl restart docker
Теперь каждый контейнер хранит максимум 30 МБ логов.
Файл подкачки (Swap) и защита от OOM
При кратковременных всплесках нагрузки механизм ядра Linux (OOM Killer) может принудительно завершить процесс базы данных или Caddy при нехватке памяти. Для защиты от сбоев создайте swap-файл на 2 ГБ с приоритетом использования оперативной памяти (vm.swappiness=10):
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Автоматические обновления безопасности
Настройте автоматическую установку критических исправлений безопасности ядра и системных пакетов без необходимости ручного обновления:
sudo apt update && sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Шаблоны проектов (FrankenPHP)
Вместо устаревшей и громоздкой связки Nginx + PHP-FPM с двумя отдельными контейнерами современный стандарт — использование FrankenPHP. Это сверхбыстрый сервер приложений на базе Caddy, который запускает PHP-код и отдаёт статику внутри одного легковесного контейнера.
Шаблон: Чистый PHP
Готовая конфигурация для легковесных скриптов и проектов на чистом PHP.
В корне проекта создайте Dockerfile для добавления необходимых PHP-расширений (утилита install-php-extensions устанавливает любые модули в одну команду):
FROM dunglas/frankenphp:php8.3-alpine
# Установка расширений (при необходимости)
RUN install-php-extensions pdo_mysql opcache
COPY . /app
Файл docker-compose.yml в папке /var/www/apps/my-app/docker-compose.yml:
services:
app:
build: .
container_name: pure_php_app
restart: unless-stopped
volumes:
- .:/app
networks:
- web_gateway
networks:
web_gateway:
external: true
В /var/www/proxy/Caddyfile проксируйте домен на порт 80 контейнера:
pure-php.example.com {
reverse_proxy pure_php_app:80
}
Шаблон: Laravel
Для приложений на Laravel настраивается точка входа в папку public, права на запись в storage и база данных MariaDB во внутренней сети.
Файл Dockerfile в корне Laravel-проекта:
FROM dunglas/frankenphp:php8.3-alpine
# Установка стандартных расширений для Laravel
RUN install-php-extensions \
pdo_mysql \
redis \
bcmath \
intl \
opcache
WORKDIR /app
COPY . /app
# Назначение прав на папки кэша и сессий пользователю deploy (UID 1000)
RUN chown -R 1000:1000 /app/storage /app/bootstrap/cache
Файл docker-compose.yml в папке /var/www/apps/laravel-app/docker-compose.yml:
services:
app:
build: .
container_name: laravel_app
restart: unless-stopped
environment:
SERVER_NAME: ":80"
CADDY_GLOBAL_OPTIONS: "auto_https off"
FRANKENPHP_CONFIG: "root /app/public"
volumes:
- .:/app
networks:
- web_gateway
- internal
depends_on:
db:
condition: service_healthy
db:
image: mariadb:11.4
container_name: laravel_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootsecret
MYSQL_DATABASE: myapp
MYSQL_USER: myapp
MYSQL_PASSWORD: secret
volumes:
- db_data:/var/lib/mysql
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD || exit 1"]
interval: 10s
timeout: 5s
retries: 5
networks:
web_gateway:
external: true
internal:
driver: bridge
volumes:
db_data:
В /var/www/proxy/Caddyfile:
laravel.example.com {
reverse_proxy laravel_app:80
}
Удаление сайта с сервера
Если проект больше не нужен, удалите его без остановки Caddy и без влияния на соседние работающие сайты:
-
Остановка контейнеров и удаление данных:
Остановите контейнеры проекта с флагом-v, чтобы гарантированно стереть связанные Docker Volumes (базу данных, кэш и постоянное хранилище):cd /var/www/apps/<проект> docker compose down -v -
Удаление файлов с диска:
Удалите директорию проекта:cd /var/www/apps rm -rf <проект> -
Отключение маршрутизации в Caddy:
Откройте/var/www/proxy/Caddyfile:nano /var/www/proxy/CaddyfileУдалите блок с доменом этого сайта и примените обновленную конфигурацию на лету:
docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile -
Очистка неиспользуемых Docker-образов:
Освободите дисковое пространство от оставшихся образов проекта:docker image prune -af -
Удаление DNS-записи:
Удалите соответствующуюA-запись домена в панели управления Cloudflare, чтобы входящий трафик не отправлялся на IP сервера.
Советы и практики
Соблюдение нескольких правил обеспечит стабильную работу мультипроектного стека и упростит его повседневное обслуживание.
Обновление и деплой
Для ручного обновления существующего проекта перейдите в его директорию и перезапустите контейнеры:
cd /var/www/apps/<проект>
git pull
docker compose build
docker compose up -d
Сайт обновится без простоя и без необходимости перезапускать Caddy или соседние сайты.
Для автоматизации обновления проектов при каждом git push без ручного ввода команд используйте руководство Автодеплой Docker-проектов на VPS через GitHub Actions и SSH.
Шпаргалка команд
- Просмотр логов Caddy:
docker compose -f /var/www/proxy/docker-compose.yml logs -f --tail=50 caddy - Диагностика ошибки 502 (Bad Gateway):
(Ошибка connection refused означает, что контейнер приложения не запущен или слушает другой внутренний порт).docker compose -f /var/www/proxy/docker-compose.yml logs --tail=20 caddy - Мониторинг ресурсов контейнеров:
docker stats - Проверка подключенных к шлюзу контейнеров:
docker network inspect web_gateway - Перезапуск конкретного проекта:
cd /var/www/apps/<проект> && docker compose restart
