Imported from yandex-ai-studio/teen-museum-residence-2026 (
skills/vm-deploy/SKILL.md). Install upstream withnpx skills add yandex-ai-studio/teen-museum-residence-2026 --skill vm-deploy. Copyright stays with the author.
VM Deploy: развёртывание Python-проекта на виртуальной машине
Скилл размещает Python-проект на виртуальной машине Yandex Cloud, доступной по SSH. Машина уже создана, её адрес, имя пользователя и файл с приватным SSH-ключом лежат в файле vm.yml (допускается vm.yaml). Проект копируется в ~/projects/<имя-проекта> на VM, там создаётся окружение через uv, устанавливаются зависимости, а на локальной машине генерируются скрипты запуска, остановки и проверки статуса (.bat на Windows, .sh на macOS/Linux), которые подключаются к VM по SSH.
Скилл рассчитан на работу с разных машин (Windows, macOS, Linux) и с разными проектами: телеграм-бот в режиме поллинга, веб-сервис, любое изолированное через uv приложение. Копирование на VM выполняется только через scp — он есть в составе OpenSSH на любых платформах, тогда как rsync/rclone на Windows обычно не установлены. Не запускай ничего на VM, пока не исправлены права на ключ.
Параметры
Параметры передаются в промпте или уточняются в чате:
-
vm_file(по умолчаниюvm.ymlв корне рабочей директории) — YAML-файл с ключамиip,user,key. Файл ищется в корне workspace; если его нет — спроси пользователя, где он. Возможный формат:ip: 51.250.28.73 user: yc-user key: team-0.keyЗначение
key— имя файла с приватным SSH-ключом; путь до ключа резолвится относительно каталогаvm_file(например,vm.ymlв корне иteam-0.keyрядом с ним). -
project_dir(обязательный) — каталог локального проекта, который нужно развернуть. -
project_name(по умолчанию — базовое имяproject_dir) — имя проекта; используется как имя удалённого каталога~/projects/<project_name>. Пробелы и спецсимволы в имени замени на-. -
entry(обязательный) — файл, который запускает проект. Определяется автоматически: проверь по очередиbot.py,main.py,app.pyв корне проекта; если ни один не найден или их несколько — уточни у пользователя. -
Тип зависимостей определяется автоматически по файлам проекта:
- есть
pyproject.toml(uv-проект) — подготовка окружения черезuv sync; - иначе есть
requirements.txt— черезuv venv+uv pip install -r requirements.txt.
- есть
Предусловия
-
Найди
vm_fileи прочитай из негоip,user,key. Проверь, что файл ключа существует по вычисленному пути. -
Убедись, что
sshдоступен локально:ssh -V. -
Проверь права на файл ключа и исправь их — без этого SSH откажется работать. Команды зависят от ОС локальной машины:
-
Windows (cmd):
icacls "<путь-до-ключа>" /inheritance:r icacls "<путь-до-ключа>" /grant:r "%username%":"(R)"В PowerShell замени
%username%на$env:USERNAME:icacls "<путь-до-ключа>" /grant:r "$env:USERNAME":"(R)" -
macOS/Linux — ключ должен быть доступен только владельцу:
chmod 600 "<путь-до-ключа>"Если ключ лежит в Git Bash/WSL на Windows — тоже используй
chmod 600.
-
-
Проверь подключение (флаг
StrictHostKeyChecking=accept-newубирает вопрос о подтверждении host key при первом входе):ssh -i "<путь-до-ключа>" -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new <user>@<ip> "echo OK"Если подключение не проходит — смотри Troubleshooting.
Шаги
-
Определи параметры проекта: найди
entry, определи тип зависимостей, проверь, есть ли в проекте.envс секретами (если есть — он нужен и на VM, но не выводи его содержимое в чат и не показывай в логах). -
Установи
uvна VM, если его нет. Бинарь ставится в~/.local/bin; в неинтерактивных ssh-сессиях PATH из профиля не подхватывается, поэтому в каждую ssh-команду, где нуженuv, добавляй export PATH:ssh -i "<путь-до-ключа>" <user>@<ip> "command -v uv || (curl -LsSf https://astral.sh/uv/install.sh | sh)"Затем проверь установку:
ssh -i "<путь-до-ключа>" <user>@<ip> "export PATH=\"\$HOME/.local/bin:\$PATH\" && uv --version"Обрати внимание:
\$HOMEи\$PATHэкранированы, чтобы локальная оболочка не подставила свои значения. Еслиcurlна VM нет — фолбэкpip install uv(при наличии pip). Когда после этого в командах используетсяuv, всегда пиши префиксexport PATH="$HOME/.local/bin:$PATH" && ...или вызывай полный путь~/.local/bin/uv. -
Создай удалённый каталог проекта:
ssh -i "<путь-до-ключа>" <user>@<ip> "mkdir -p ~/projects/<project_name>" -
Скопируй проект на VM только через
scp. Никогда не копируй на VM файлvm_fileи приватные ключи (*.key) — удали их из каталога проекта перед копированием или после неё.scp -rне умеет исключать файлы, поэтому сначала копируется весь каталог, потом на VM удаляется лишнее:scp -r -i "<путь-до-ключа>" "<project_dir>" <user>@<ip>:~/projects/ ssh -i "<путь-до-ключа>" <user>@<ip> "cd ~/projects/<project_name> && rm -rf -- .venv .git __pycache__ .pytest_cache"Удаляй на VM ровно то, что присутствует локально и не должно попасть на VM: окружение и мусор (
.venv,.git,__pycache__,.pytest_cache), а такжеvm.yml,vm.yamlи любые*.keyв каталоге проекта.scp -rкопирует и скрытые файлы, поэтому.envокажется на VM автоматически, если он есть вproject_dir.После копирования проверь состав удалённого каталога:
ssh ... "ls -la ~/projects/<project_name>". Файл.envдолжен оказаться на VM (если был локально),vm.ymlи ключи — нет. -
Создай окружение на VM в зависимости от типа проекта:
-
uv-проект (
pyproject.toml):ssh -i "<путь-до-ключа>" <user>@<ip> "export PATH=\"\$HOME/.local/bin:\$PATH\" && cd ~/projects/<project_name> && uv venv && uv sync" -
проект с
requirements.txt:ssh -i "<путь-до-ключа>" <user>@<ip> "export PATH=\"\$HOME/.local/bin:\$PATH\" && cd ~/projects/<project_name> && uv venv && uv pip install -r requirements.txt"Если
uvне находит нужную версию Python — uv сам скачает managed Python при вызовеuv venv; при необходимости добавь явноuv python install <версия>.
-
-
Сгенерируй скрипты. В каталоге проекта на локальной машине создай удалённый помощник
deploy.shи тонкие обёртки над ним:start.bat,stop.bat,status.bat(Windows) иstart.sh,stop.sh,status.sh(Unix). Вся логика (запуск черезuv run, остановка через kill) живёт вdeploy.sh— так удаётся избежать проблем с экранированием кавычек между cmd/PowerShell и ssh. Значенияip,user, путь к ключу и имя проекта подставь в обёртки как есть, без чтенияvm.ymlв рантайме. Содержимое файлов — ниже.deploy.sh(копируется на VM и запускается удалённо): он сам подправляет PATH, переходит в свой каталог и умеетstart | stop | status:#!/usr/bin/env bash set -u export PATH="$HOME/.local/bin:$PATH" cd "$(cd "$(dirname "$0")" && pwd)" ENTRY="<entry>" case "${1:-}" in start) # setsid создаёт отдельную группу процессов: kill группы потом остановит и uv, и python setsid nohup uv run python "$ENTRY" >> app.log 2>&1 < /dev/null & echo $! > app.pid echo "started, pid=$(cat app.pid), log: $(pwd)/app.log" ;; stop) pid="$(cat app.pid 2>/dev/null || true)" if [ -n "$pid" ]; then kill -- -"$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true fi pkill -f "python .*${ENTRY}" 2>/dev/null || true rm -f app.pid echo "stopped" ;; status) pid="$(cat app.pid 2>/dev/null || true)" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "running, pid=$pid" ps -o pid,etime,cmd -p "$pid" echo "--- app.log (last 20 lines) ---" tail -n 20 app.log 2>/dev/null || true else echo "not running" fi ;; *) echo "usage: deploy.sh {start|stop|status}" >&2 exit 1 ;; esacЕсли на VM нет
setsid(проверьcommand -v setsid) —kill -- -pidне сработает, и остановка сработает через фолбэкиkill "$pid"иpkill -f "python .*<entry>".start.bat(Windows), в<...>— реальные значения изvm.ymlи имя проекта:@echo off ssh -i "<абсолютный-путь-до-ключа>" -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new <user>@<ip> "bash ~/projects/<project_name>/deploy.sh start" pausestop.bat:@echo off ssh -i "<абсолютный-путь-до-ключа>" -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new <user>@<ip> "bash ~/projects/<project_name>/deploy.sh stop" pausestatus.bat— то же, но сdeploy.sh status.start.sh(macOS/Linux):#!/usr/bin/env bash ssh -i "<абсолютный-путь-до-ключа>" -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new <user>@<ip> "bash ~/projects/<project_name>/deploy.sh start"stop.shиstatus.sh— по аналогии (замениstartнаstop/status). Сделай.sh-файлы исполняемыми:chmod +x start.sh stop.sh status.sh.Скопируй
deploy.shна VM и сделай исполняемым (заодно завершится шаг генерации):scp -i "<путь-до-ключа>" "<project_dir>/deploy.sh" <user>@<ip>:~/projects/<project_name>/deploy.sh ssh -i "<путь-до-ключа>" <user>@<ip> "chmod +x ~/projects/<project_name>/deploy.sh"Сгенерированные скрипты не содержат секретов, их можно коммитить; если не хочется — добавь их в
.gitignoreпроекта. -
Запусти проект и проверь:
ssh -i "<путь-до-ключа>" <user>@<ip> "bash ~/projects/<project_name>/deploy.sh status"Должно быть
running, pid=.... Посмотри хвост лога целиком:ssh ... "tail -n 50 ~/projects/<project_name>/app.log". В логе не должно быть исключений и ошибок запуска.Проверка зависит от типа проекта:
- Телеграм-бот (поллинг) — попроси пользователя написать боту
/startи убедись, что он отвечает; при необходимости ещё раз посмотри лог. - Веб-сервис — на VM проверь, что процесс слушает порт:
ssh ... "ss -tlnp | grep python", и что отвечает локально:ssh ... "curl -sI http://127.0.0.1:<порт>". Для доступа извне напомни пользователю про правила группы безопасности (security group) в консоли Yandex Cloud: входящий трафик на нужный порт должен быть разрешён, иначе сайт не откроется по публичному IP, хотя на самой VM он работает.
- Телеграм-бот (поллинг) — попроси пользователя написать боту
-
Итог — сообщи пользователю: имя проекта на VM (
~/projects/<project_name>), как запускать (start.bat/start.sh), останавливать и смотреть статус, путь к логуapp.log.
Обновление проекта (re-deploy)
Развёртывание повторяется по шагам 2–7. Сначала останови старый процесс, потом обнови файлы и запусти заново:
ssh ... "bash ~/projects/<project_name>/deploy.sh stop"- Скопируй изменённые файлы на VM через
scp. Заново перекопируй весь каталог (команда из шага 4, включая чистку на VM) либо только изменённые файлы по одному:scp -i "<ключ>" <файл> <user>@<ip>:~/projects/<project_name>/. - При изменении зависимостей повтори шаг 5 (создание окружения).
ssh ... "bash ~/projects/<project_name>/deploy.sh start", затем проверь статус и лог.
Чек-лист
vm_fileнайден,ip/user/keyпрочитаны, путь до ключа верный.- Права на ключ исправлены: Windows —
icacls /inheritance:r+/grant:r, Unix —chmod 600. ssh "echo OK"проходит сStrictHostKeyChecking=accept-new.uvустановлен на VM и работает (uv --versionчерезexport PATH="$HOME/.local/bin:$PATH").- Проект лежит в
~/projects/<project_name>; там нетvm.yml, приватных ключей,.venv,.git;.envна месте. - Окружение создано (
uv syncилиuv pip install -r requirements.txt), приложение стартует без ошибок. - В проекте сгенерированы
deploy.sh(загружен на VM) и обёрткиstart/stop/statusдля платформы пользователя. deploy.sh statusпоказываетrunning, вapp.logнет ошибок; проверка для бота/веб-сервиса пройдена (или пользователю сообщено, что нужно сделать).
Troubleshooting
- Если появляется ошибка соединения с api.telegram.org - используй явный ip-адрес 149.154.167.220, можно прописать такой адрес в /etc/hosts (но лучше предварительно убедиться, что с ним есть соединение)
UNPROTECTED PRIVATE KEY FILEилиPermissions 0644 ... are too open— ключ доступен другим пользователям; исправь права:chmod 600(Unix) или icacls-команды из Предусловий (Windows).- Подключение висит на запросе
Are you sure you want to continue connecting— первый вход; используй-o StrictHostKeyChecking=accept-newодин раз, чтобы принять host key автоматически. uv: command not foundпри запуске из-под ssh — в неинтерактивной сессии не подгружается PATH из профиля; добавляйexport PATH="$HOME/.local/bin:$PATH" && ...в начало команды или используй полный путь~/.local/bin/uv.Host key verification failed— host key изменился (пересоздание VM); удали старую записьssh-keygen -R <ip>и повтори подключение сaccept-new.- После
stopпроцесс ещё жив — проверьps -ef | grep <entry>; если процессы запускались внеsetsid— добей вручную:pkill -f "python .*<entry>". setsid: command not found— утилита отсутствует на VM; остановка сработает через фолбэкkill $(cat app.pid)+pkill.- Проект запущен, но внешне не отвечает (веб) — проверь
ss -tlnpна VM (слушает ли порт, не только 127.0.0.1) и правила security group в консоли Yandex Cloud. permission deniedприscp— проверь права на удалённый каталог и его владельца (ls -la ~/projects).curl: command not foundна VM — используйpython3 -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:<порт>').status)".- Проблемы с зависимостями — убедись, что на VM работает тот же механизм установки, что и локально (
uv syncдля uv-проекта, иначеrequirements.txt); при несовпадении версии Python uv сам скачает managed Python.