Введение
Эта инструкция — пошаговый воспроизводимый шаблон настройки чистого VPS (Ubuntu) для одновременной работы нескольких изолированных проектов.
Суть архитектуры:
- Наружу открыты только порты
80и443. - Единственная точка входа — легковесный реверс-прокси Caddy, который берет на себя маршрутизацию и автоматический выпуск SSL-сертификатов.
- Проекты изолированы в своих
docker-composeфайлах и общаются с Caddy через единую виртуальную сетьweb_gateway. Базы данных и служебные сервисы наружу не светятся. - Все операции выполняются под выделенным пользователем
deployс правами1000:1000.
Следуя шагам ниже, вы получите готовую к продакшену инфраструктуру, куда новые сайты добавляются за пару минут.
1. Пользователь deploy и директории
Все действия на сервере выполняются под пользователем deploy, а не root.
1.1. Создание пользователя
Под пользователем root:
adduser deploy
usermod -aG sudo deploy
usermod -aG docker deploy
Переключитесь на пользователя deploy:
su - deploy
1.2. Создание структуры папок
sudo mkdir -p /var/www/proxy /var/www/apps
sudo chown -R deploy:deploy /var/www
Важно
2. Общая Docker-сеть
Создайте внешнюю сеть-мост для связи Caddy с веб-контейнерами проектов:
docker network create web_gateway
3. Центральный реверс-прокси Caddy
3.1. Создание конфигурации 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
}
3.2. Запуск и перезагрузка
Запустите Caddy:
docker compose -f /var/www/proxy/docker-compose.yml up -d
Команда перезагрузки
4. Настройка Cloudflare
Для корректной работы SSL и защиты трафика выставьте настройки в панели Cloudflare:
- SSL/TLS -> Overview: установите режим Full (strict).
(Режим Flexible вызывает бесконечный редирект 301 между Cloudflare и Caddy). - 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
}
5. SSH-доступ к GitHub
Под пользователем deploy сгенерируйте ключ для доступа к репозиториям:
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 и выполните проверку:
ssh -T git@github.com
6. Шаблон проекта: Laravel / PHP + Nginx
При развертывании классического стека важно учесть 3 момента:
- Контейнеры не публикуют порты наружу (
ports:отсутствуют). - Веб-сервер
webподключен к сетямweb_gatewayи внутреннейinternal. Служебные сервисы (app,db,redis) доступны только вinternal. - В официальном образе
nginx:alpineобязательно задается переменнаяNGINX_ENVSUBST_FILTER, иначе утилита шаблонизации сотрет системные переменные Nginx ($uri,$fastcgi_script_name) и вызовет ошибку 500.
6.1. Пример docker-compose.yml проекта
В папке /var/www/apps/my-app/docker-compose.yml:
services:
web:
image: nginx:alpine
container_name: myapp_web
restart: unless-stopped
environment:
PROJECT_NAME: myapp
NGINX_ENVSUBST_FILTER: PROJECT_NAME
volumes:
- ./docker/nginx/templates:/etc/nginx/templates:ro
- ./public:/var/www/html/public:ro
- storage_data:/var/www/html/storage:ro
networks:
- internal
- web_gateway
depends_on:
- app
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapp_app
restart: unless-stopped
volumes:
- .:/var/www/html
- storage_data:/var/www/html/storage
networks:
- internal
depends_on:
db:
condition: service_healthy
db:
image: mariadb:11.4
container_name: myapp_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:
storage_data:
db_data:
6.2. Нюансы прав в Dockerfile
Чтобы PHP-FPM мог беспрепятственно писать в кэш и логи, согласуйте UID системного пользователя www-data с пользователем deploy (1000):
ARG UID=1000
ARG GID=1000
RUN apk add --no-cache shadow \
&& usermod -u ${UID} www-data \
&& groupmod -g ${GID} www-data
7. Шаблон составного проекта (Next.js + Fastify API)
Если проект состоит из раздельных контейнеров фронтенда и API:
- Оба контейнера включаются в сеть
web_gatewayи получают фиксированные имена. - В
/var/www/proxy/Caddyfileмаршруты разделяются через директивыhandle:
api-app.example.com {
handle /api/* {
reverse_proxy app_backend:3001
}
handle {
reverse_proxy app_frontend:3000
}
}
502 Bad Gateway
8. Удаление сайта с сервера
Если проект больше не нужен, удалите его без остановки 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 сервера.
Вывод
Вся инфраструктура настроена и работает как единый шлюз. Ниже алгоритм повседневной эксплуатации сервера.
1. Добавление нового сайта за 3 шага
- DNS: Создайте
A-запись домена в Cloudflare на IP вашего VPS (оранжевое облако). - Запуск контейнера: В папке проекта (
/var/www/apps/<проект>) укажите вdocker-compose.ymlсетьweb_gatewayи имя веб-контейнера:cd /var/www/apps/<проект> docker compose up -d - Маршрутизация в Caddy: Добавьте сайт в
/var/www/proxy/Caddyfile:
И примените конфигурацию:newsite.com { reverse_proxy <имя_веб_контейнера>:80 }
Caddy мгновенно выпустит SSL-сертификат и начнет проксировать трафик.docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile
2. Обновление существующего проекта (деплой)
В директории проекта:
cd /var/www/apps/<проект>
git pull
docker compose build
docker compose up -d
Сайт обновится без простоя и без необходимости трогать Caddy или соседние сайты.
Для настройки непрерывного автоматического обновления проектов при git push без ручного ввода команд используйте руководство Автодеплой Docker-проектов на VPS через GitHub Actions и SSH.
3. Шпаргалка команд
- Просмотр логов Caddy:
docker compose -f /var/www/proxy/docker-compose.yml logs -f --tail=50 caddy - Проверка подключенных к шлюзу контейнеров:
docker network inspect web_gateway - Перезапуск конкретного проекта:
cd /var/www/apps/<проект> && docker compose restart