
Можно ли на один VPS поставить несколько сервисов? Можно, все так делают — у меня на личном VPS крутятся таск-трекер, бот в Telegram, пара пет-проектов и Grafana, чтобы за всем этим следить. Однако немногие знают, как это сделать правильно, поэтому рассказываю под катом.
VPS — это машина с root-доступом, поэтому ничего не мешает запускать на ней хоть десять проектов одновременно, были бы ресурсы. Весь веб-хостинг девяностых, кстати, начался ровно с этого — в 1996 году Apache научился отдавать разные сайты по разным доменным именам с одного сервера. В связи с этим сразу перехожу к вопросу «Как?».
HTTP-запрос содержит доменное имя внутри, в заголовке Host. Когда пользователь открывает tracker.example.com и bot.example.com, оба запроса прилетают на один IP и один порт 443, но с разными именами. Значит, на входе нужен диспетчер — это называется обратный прокси. Сразу отмечу, не копируйте слепо мои команды, они с моими примерами, адаптируйте или адаптируйтесь.
Способ первый. Диспетчер на входе
В роли диспетчера обычно выступает nginx, Caddy или Traefik. Сервисы слушают свои внутренние порты, а наружу смотрит только прокси. Он же занимается TLS-сертификатами, редиректом с HTTP на HTTPS и отдаёт статику.
Если хотите минимум возни, обратите внимание именно на Caddy. Он легкий и у него короткий конфиг:
reverse_proxy localhost:3000
}
reverse_proxy localhost:8080
}
Это реально вся конфигурация — Caddy сам настраивает HTTPS, получает сертификаты, например Let's Encrypt или ZeroSSL, и продлевает их. Для этого DNS-записи доменов должны указывать на сервер, а необходимые порты должны быть доступны извне.
С nginx конфигурация длиннее, зато гибче и привычнее многим. Для каждого домена тут создается отдельный блок server, в котором указываются порт, доменное имя и адрес приложения:
server { listen 80; server_name tracker.example.com; location / { proxy_pass http://127.0.0.1:3000; 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; } } server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:3001; 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; } }
Здесь listen 80 указывает порт, на котором nginx принимает обычные HTTP-запросы, а server_name определяет, какому домену соответствует каждый блок. Директива proxy_pass передаёт запросы нужному приложению, причём оба сервиса слушают только локальный интерфейс 127.0.0.1.
Два таких блока в конфиге, и вы получите два независимых сервиса на одной машине, каждый со своим доменом. Конфиг кладём в /etc/nginx/sites-available/ или conf.d/, дальше проверка синтаксиса и аккуратный перезапуск:
sudo nginx -t && sudo systemctl reload nginx
Если nginx обнаружит ошибку, исправьте указанную проблему и повторите проверку. Для подключения HTTPS можно использовать Certbot с плагином nginx — он получит TLS-сертификат, добавит необходимые настройки для порта 443 и сможет автоматически продлевать сертификат:
sudo certbot --nginx -d tracker.example.com -d api.example.com
Для этого DNS-записи обоих доменов должны указывать на сервер, входящие подключения к порту 80 должны быть разрешены, а Certbot и его плагин nginx — установлены. После проверьте автоматическое продление сертификатов:
sudo certbot renew --dry-run
Затем посмотрите, кто вообще что слушает на машине, и убедитесь, что наружу торчат только 80, 443 и SSH, а сервисы сидят на localhost:
sudo ss -tulpn
В выводе у tracker и api (это мои примеры) в колонке Local Address должно быть 127.0.0.1:3000 и 127.0.0.1:3001, а не 0.0.0.0, иначе их порты открыты всему интернету в обход прокси и файрвола.
Если приложение умеет биндиться на адрес, пропишите в его конфиге 127.0.0.1. И, пожалуйста, разносите сервисы по поддоменам, а не по путям вроде example.com/tracker. С поддоменами меньше головной боли с куками, редиректами и путями статики внутри приложений.
Способ второй. Каждому сервису по контейнеру
Когда у сервисов разные зависимости, лучше упаковать каждый в свой docker-compose стек, а поверх поставить один общий прокси, который слушает внешние порты и раскидывает запросы по контейнерам. Для этого нужно создать общую внутреннюю сеть (это делается один раз):
docker network create front
Дальше один compose-файл с nginx или Caddy как входной точкой, и рядом по одному compose на каждый сервис, все они подключены к этой сети:
services: proxy: image: caddy:2 ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config restart: unless-stopped networks: - front tracker: image: my-tracker:latest mem_limit: 512m restart: unless-stopped networks: - front networks: front: external: true volumes: caddy_data: caddy_config:
Обратите внимание, что наружу опубликованы только порты прокси. Таск-трекер доступен другим контейнерам в сети front на своем внутреннем порту 3000, а mem_limit ограничивает потребление памяти, чтобы утечка в приложении не съела всю машину.
Конфиг Caddy писал выше. Для контейнеров его нужно немного изменить — вместо localhost использовать имя сервиса внутри Docker-сети. В файл Caddyfile, который лежит рядом с compose.yaml, добавляем:
reverse_proxy tracker:3000
}
Запуск и проверка, что всё поднялось так:
docker compose up -d docker compose ps
Сумму жёстких лимитов держите ниже физической памяти машины, иначе вы просто отложили проблему. И не забудьте оставить запас ядру, SSH, Docker-демону и прокси. Сколько каждый контейнер реально ест памяти и CPU можно посмотреть через команду:
docker stats --no-stream
Плюс подхода в изоляции, зависимости одного сервиса не ломают другой, обновляете каждый отдельно. Минус — у каждого контейнера свой оверхед по памяти, ну и добавляется управление контейнерами, образами, сетями и томами.
Способ третий. Обычные процессы под разными пользователями
Самый старый вариант — это запустить каждый сервис обычным процессом, но под своим пользователем и под надзором systemd, который перезапустит упавшее и запишет логи. Создаём системного пользователя без шелла и домашней папки:
sudo useradd -r -U -s /usr/sbin/nologin tracker sudo install -d -o tracker -g tracker /opt/tracker
После размещения приложения в /opt/tracker проверьте права на исполняемый файл run.sh, каталоги с данными и используемые конфигурационные файлы. Сервисный пользователь должен иметь доступ только к необходимым ресурсам:
И пишем юнит /etc/systemd/system/tracker.service:
[Unit]
Description=My tracker
After=network.target
[Service]
User=tracker
WorkingDirectory=/opt/tracker
ExecStart=/opt/tracker/run.sh
Restart=always
MemoryMax=512M
OOMScoreAdjust=500
[Install]
WantedBy=multi-user.target
MemoryMax — это жёсткий потолок памяти для сервиса, аналог mem_limit из контейнерного мира, а OOMScoreAdjust=500 повышает вероятность выбора процесса системным OOM killer. Также помните про MemoryHigh. Включается всё командами:
sudo systemctl daemon-reload sudo systemctl enable --now tracker sudo systemctl status tracker
Вывод сервиса попадает в системный журнал с фильтрацией. Например, команда ниже показывает сообщения tracker:
journalctl -u tracker -f
К слову, разные пользователи заодно дают базовую изоляцию по файлам, один сервис физически не может прочитать конфиги другого без лишних прав. Для двух-трёх своих сервисов на одном VPS это, по мне, недооценённый вариант.
Сколько сервисов влезет и как проверить
Четкий ответ не дать, зависит от вашей VPS. По моим оценкам, лёгкий Go- или Node-сервис в простое ест 50–150 МБ, Python-приложение обычно 150–400 МБ, а PostgreSQL для личных проектов нормально живёт в 300–500 МБ, если не забыть подрезать shared_buffers. Потребление памяти Redis зависит от объёма хранимых данных, структуры ключей и служебных расходов. Ну и Grafana с Prometheus могут попросить ещё полгига.
Итого на VPS с 2 гигабайтами могут жить четыре-пять лёгких сервисов без базы или два-три с базой. Личные проекты простаивают большую часть времени, и CPU отлично делится между ними по остаточному принципу. Конфигурацию надо проверять — реальное потребление каждого сервиса смотрите командой:
ps -eo pid,comm,rss --sort=-rss | head -15 free -h
Для первоначальной оценки смотрите на ps и free -h, а в более точном анализе учитывайте rss и потребление групп процессов через cgroups. Грубо говоря, прикиньте rss сервиса под нагрузкой, плюс запас в 30% на пики, плюс память базе, плюс саму систему с её кэшами. Если сумма влезает в объём машины с запасом, заселяйте, если впритык — либо апгрейд тарифа, либо расселение.
Также важно соседство по времени. Ночной бэкап одного сервиса упрётся в диск и задушит сайт другого. Лечится расписаниями и понижением приоритета фоновых задач, тот же pg_dump в кроне запускается с низким приоритетом по диску и процессору:
#!/usr/bin/env bash set -euo pipefail BACKUP_DIR=/backups DATE=$(date +%F) ionice -c 3 nice -n 19 \ pg_dump -U app -d appdb \ -Fc -f "$BACKUP_DIR/appdb_$DATE.dump"
Предварительно создайте каталог /backups, настройте права доступа и аутентификацию пользователя PostgreSQL, а скрипт сохраните в /usr/local/bin/backup-appdb.sh с разрешением на выполнение. После этого его можно запускать через cron:
0 3 * * * /usr/local/bin/backup-appdb.sh
Для регулярной эксплуатации нужны контроль успешного завершения, ротация архивов и отправка резервной копии в другое хранилище.
Про безопасность
Общая машина — это общий периметр. Контейнеры и разные пользователи снижают риски, но ядро у всех одно, поэтому взлом одного дырявого сервиса может открыть дорогу к соседям. Минимально сделайте так, чтобы файрвол пропускал только веб и SSH, а всё остальное было закрыто:
sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 22/tcp sudo ufw default deny incoming sudo ufw enable
Если вы подключены к VPS по SSH, убедитесь, что разрешен именно тот порт, на котором работает SSH-сервер. Ошибка в правилах может привести к потере удаленного доступа.
Также помните, что при публикации контейнерных портов Docker сам создает правила сетевой фильтрации и перенаправления трафика — правила UFW могут не блокировать доступ к опубликованным портам. В связи с этим, контейнеры с БД и внутренними API вообще не публикуйте наружу, а ограничения доступа сверьте с сетевой конфигурацией.
Из ИБ-правил побольше я придерживаюсь двух:
Тестовые и экспериментальные поделки не должны жить на той же машине, что и сервис с реальными пользователями и их данными. Для экспериментов нужна отдельная дешёвая виртуалка, которую не жалко снести целиком.
Чем старше и неизвестнее софт, тем сильнее изоляция. Пятилетний WordPress с самописными плагинами и свежий Go-сервис — это разные уровни доверия, и сажать их рядом я не советую.
Бэкапы и мониторинг лучше отправить к соседям
Снапшот VPS помогает быстро откатиться перед обновлением, но резервную копию всё равно отправляйте в другое хранилище (снапшот — это не бэкап).
Для PostgreSQL дополнительно используйте pg_dump или pg_dumpall, помня, что второй сохраняет и роли. Для файлов подойдут Restic, Borg или Kopia — главное восстановить копию на тестовой машине.
Мониторинг на том же VPS полезен для метрик и логов, но он не сообщит о полной смерти машины. Например, Uptime Kuma проверяет HTTP, TCP, DNS и сертификаты, только не размещайте её рядом с наблюдаемыми сайтами. Заводите в ней монитор на каждый домен из списка, подключаете Телеграм-уведомления, и сообщения о падении соседей будут приходить автоматически, с задержкой, зависящей от настроек проверок. Также не забывайте смотреть на потребление машины в целом.
Когда пора расселять
Среди логичных причин разъехаться:
разные владельцы или уровни доверия;
ресурсы, когда один сервис регулярно мешает остальным даже после настройки лимитов;
деньги и обещания клиентам — если простой одного приложения останавливает оплату в другом, общая машина не поможет сэкономить.
Также обратите внимание на требования к обновлениям. Если одному проекту нужно часто менять Docker и ядро, а другой должен месяцами работать без перезагрузок, лучше развести.
Если упёрлись только в память или диск, второй сервер тоже не всегда нужен. Иногда проще увеличить текущий. У RUVDS параметры VPS можно менять без переноса данных в пределах доступных конфигураций, а VPS с 2 vCPU, 4 ГБ RAM и 40 ГБ SSD стоит 1149 рублей в месяц при оплате сразу за три месяца. Такой машины хватит для нескольких сервисов, если не запускать на ней очередной локальный ИИ и не считать 4 ГБ бесконечностью.
Делить лучше по последствиям
В итоге на одном VPS собирайте сервисы, которым вы доверяете одинаково и чей одновременный простой готовы пережить. Разводите сервисы прокси по доменам или контейнерами, ограничивайте каждый по памяти, считайте потребление с запасом, не смешивайте бой с полигоном, и одна средняя машина заменит вам четыре.
А сколько сервисов крутится на вашей основной VPS, и был ли случай, когда один сосед уронил остальных? Делитесь историями в комментариях.
© 2026 ООО «МТ ФИНАНС»
ex_ineris
Я на своей VPS кручу mail server vpn server wire guard telega bot все это в докер контейнерах 2гб ОЗУ два ядра и 40гб ssd вполне хватает, можно что то ещё но пока не придумал, а и fail2ban что бы боты не лезли в бан на год по айпи банит, а самое главное не написал касательно темы сабжа, nginx proxy manager рулит и claud flare с CNAME.
andreymal
Хм, а какой у вас banaction? Я у себя поставил бан всего 7 дней, но этого уже достаточно, чтобы systemctl stop fail2ban задумывался на несколько минут и тем самым создавал проблемы
dmitrychou
atlas@mail:~$ sudo fail2ban-client status sshd | egrep “Currently banned|Total banned” |- Currently banned: 11649 |-
Total banned: 11649
atlas@mail:~$
Вообще не создает проблем, наооборот из-за постояннного перебора и флуда на порты, были проблемы, сейчас с fail2ban и блоком все гуд.