Запуск Qwen3 на VPS по API с TOKEN. 1 CPU, 2GB RAM
Нужен максимально простой запуск без proxy, токенов и API? Есть отдельная упрощённая инструкция: Быстрый запуск Qwen3 на VPS без GPU .
Она подойдёт, если нужно просто поднять Qwen3-1.7B на слабом VPS и начать пользоваться моделью локально.
Технические детали
Модель поднималась на VPS от Бегет:
Процессор: 1 виртуальное ядро Intel Xeon
Оперативная память: 2 ГБ
Swap: 2 ГБ
Диск: 15 ГБ NVMe
OS: Ubuntu
Модель: Qwen3-1.7B, квантование Q4_K_M
Запуск: llama.cpp
Команды для запуска модели
mkdir -p /root/llama
cd /root/llama
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo apt update
sudo apt install build-essential git cmake python3-pip python3-venv wget curl -y
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
mkdir build
cd build
cmake ..
cmake --build . --config Release -j1
python3 -m venv /root/hfvenv
/root/hfvenv/bin/pip install -U "huggingface_hub[cli]"
mkdir -p /root/qwen3
/root/hfvenv/bin/hf download \
unsloth/Qwen3-1.7B-GGUF \
Qwen3-1.7B-Q4_K_M.gguf \
--local-dir /root/qwen3
/root/llama/llama.cpp/build/bin/llama-cli \
-m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf \
-t 1 \
-c 1024 \
-n 120 \
--temp 0.7 \
--no-conversation \
-p "Ты мастер текстовой RPG. Отвечай кратко.
/no_think
Игрок: осматриваю комнату.
Мастер:"
После ввода команды '/root/llama/llama.cpp/build/bin/llama-cli \
-m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf \
-t 1 \
-c 1024 \
-n 120 \
--temp 0.7 \
--no-conversation \
-p "Ты мастер текстовой RPG. Отвечай кратко.
/no_think
Игрок: осматриваю комнату.
Мастер:"'
нужно не забыть нажать Enter )))) потому что сначала я подумал что модель не взлетела и зависла...
Запуск API для генерации текста
Для ручной проверки подходит llama-cli. Но для сайта или Telegram-бота нужен HTTP API. Проще всего использовать встроенный llama-server из llama.cpp.
Но напрямую наружу llama-server лучше не открывать. В нём нет простой токен-авторизации. Поэтому делаем схему из двух сервисов.
Внешний запрос → Flask proxy на порту 8092 с проверкой токена → llama-server на 127.0.0.1:8093 → Qwen3 возвращает текст
Так llama-server доступен только внутри VPS. Снаружи виден только proxy. Без токена proxy не даст выполнить генерацию.
Ручной запуск llama-server
Сначала проверяем, что модель запускается как локальный API. Важно указать именно 127.0.0.1, а не 0.0.0.0. Тогда сервер модели не будет доступен извне.
/root/llama/llama.cpp/build/bin/llama-server \ -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf \ -t 1 \ -c 1024 \ --host 127.0.0.1 \ --port 8093
В другой SSH-консоли проверяем health endpoint:
curl http://127.0.0.1:8093/health
Ожидаемый ответ:
{"status":"ok"}
С компьютера этот порт открываться не должен. Это нормально. Порт 8093 нужен только внутри VPS.
Установка Flask proxy
Proxy будет принимать внешние запросы на порту 8092. Он проверит заголовок X-LLM-Token. Потом перешлёт запрос во внутренний llama-server.
mkdir -p /root/llm_proxy cd /root/llm_proxy python3 -m venv venv /root/llm_proxy/venv/bin/pip install flask gunicorn requests
Создаём файл приложения:
nano /root/llm_proxy/app.py
Содержимое файла:
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
app.json.ensure_ascii = False
TOKEN = "YOUR_TOKEN"
LLAMA_URL = "http://127.0.0.1:8093/completion"
@app.route("/health", methods=["GET"])
def health():
return jsonify({
"ok": True,
"service": "llm_proxy"
})
@app.route("/generate", methods=["POST"])
def generate():
token = request.headers.get("X-LLM-Token", "")
if token != TOKEN:
return jsonify({
"ok": False,
"error": "unauthorized"
}), 401
data = request.get_json(silent=True)
if not isinstance(data, dict):
return jsonify({
"ok": False,
"error": "json body is required"
}), 400
if "prompt" not in data:
return jsonify({
"ok": False,
"error": "prompt is required"
}), 400
data["prompt"] = "/no_think " + str(data["prompt"])
if "n_predict" not in data:
data["n_predict"] = 80
try:
r = requests.post(
LLAMA_URL,
json=data,
timeout=300
)
return r.text, r.status_code, {
"Content-Type": "application/json"
}
except Exception as e:
return jsonify({
"ok": False,
"error": "proxy error",
"details": str(e)
}), 500
Ручной запуск proxy
Пока llama-server работает на 8093, запускаем proxy на 8092.
cd /root/llm_proxy /root/llm_proxy/venv/bin/gunicorn \ -w 1 \ -b 0.0.0.0:8092 \ --timeout 320 \ app:app
Проверка health endpoint с компьютера:
curl http://VPS_IP:8092/health
Ожидаемый ответ:
{"ok":true,"service":"llm_proxy"}
Проверка генерации:
curl -X POST http://VPS_IP:8092/generate -H "Content-Type: application/json" -H "X-LLM-Token: YOUR_TOKEN" -d "{\"prompt\":\"Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.\",\"n_predict\":80}"
Если убрать токен, должен прийти отказ:
{"error":"unauthorized","ok":false}
Автозапуск llama-server через systemd
После ручной проверки останавливаем llama-server и proxy через Ctrl+C. Теперь создаём systemd-сервисы. Они будут запускаться в фоне и подниматься после перезагрузки VPS.
Создаём сервис модели:
nano /etc/systemd/system/llama-server.service
Содержимое файла:
[Unit] Description=Qwen3 llama-server After=network.target [Service] Type=simple WorkingDirectory=/root/llama/llama.cpp/build ExecStart=/root/llama/llama.cpp/build/bin/llama-server -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf -t 1 -c 1024 --host 127.0.0.1 --port 8093 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
Автозапуск proxy через systemd
Создаём второй сервис. Он зависит от llama-server. Это нужно, чтобы proxy стартовал после сервера модели.
nano /etc/systemd/system/llm-proxy.service
Содержимое файла:
[Unit] Description=LLM token proxy After=network.target llama-server.service Requires=llama-server.service [Service] Type=simple WorkingDirectory=/root/llm_proxy ExecStart=/root/llm_proxy/venv/bin/gunicorn -w 1 -b 0.0.0.0:8092 --timeout 320 app:app Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
Запуск сервисов
systemctl daemon-reload systemctl enable llama-server systemctl enable llm-proxy systemctl start llama-server systemctl start llm-proxy
Проверяем статус:
systemctl status llama-server systemctl status llm-proxy
Проверяем порты:
ss -tulpn | grep 809
Должно быть примерно так:
127.0.0.1:8093 llama-server 0.0.0.0:8092 gunicorn
Если на этом же сервере стоит STT API, то ещё будет порт 8091.
0.0.0.0:8091 gunicorn
Проверка после systemd
На самой VPS:
curl http://127.0.0.1:8093/health curl http://127.0.0.1:8092/health
С компьютера или с сервера бота:
curl http://VPS_IP:8092/health
Генерация с самой машины:
curl -X POST http://127.0.0.1:8093/completion \
-H "Content-Type: application/json" \
-d '{"prompt":"/no_think Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.","n_predict":150}'
Генерация через proxy:
curl -X POST http://VPS_IP:8092/generate -H "Content-Type: application/json" -H "X-LLM-Token: YOUR_TOKEN" -d "{\"prompt\":\"Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.\",\"n_predict\":80}"

Добрая моделька. Позитивная...
Просмотр логов
Если модель не стартует, смотрим логи llama-server:
journalctl -u llama-server -f
Если proxy отвечает ошибкой, смотрим логи proxy:
journalctl -u llm-proxy -f
PHP-запрос к API
На сервере бота можно вызывать proxy обычным POST-запросом. В заголовок передаётся токен. В тело запроса передаётся prompt и лимит токенов.
<?php
function generateLocalLlmText($prompt, $max_tokens = 80)
{
$url = 'http://VPS_IP:8092/generate';
$data = array(
'prompt' => $prompt,
'n_predict' => $max_tokens
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'X-LLM-Token: YOUR_TOKEN'
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
return array(
'ok' => false,
'error' => $error
);
}
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$json = json_decode($response, true);
if (!is_array($json)) {
return array(
'ok' => false,
'error' => 'bad json',
'http_code' => $http_code,
'raw' => $response
);
}
if (!empty($json['content'])) {
return array(
'ok' => true,
'text' => trim($json['content']),
'http_code' => $http_code
);
}
return array(
'ok' => false,
'error' => 'empty response',
'http_code' => $http_code,
'raw' => $json
);
}
$res = generateLocalLlmText('Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.', 80);
print_r($res);
?>
Что важно помнить
Qwen3 может включать режим рассуждений. Поэтому proxy автоматически добавляет /no_think в начало prompt. Это убирает внутренние рассуждения из ответа и экономит время генерации.
На слабом VPS лучше держать один поток и небольшой контекст. Поэтому используются параметры -t 1 и -c 1024. Для коротких задач этого достаточно.
Порт 8093 не должен быть доступен извне. Это внутренний порт llama-server. Снаружи используется только 8092, где стоит проверка токена.
Смена модели без пересборки API
Если proxy уже настроен, его не нужно удалять. Он отвечает только за внешний API и проверку токена. Для смены модели достаточно остановить сервисы, заменить путь к GGUF-файлу и запустить всё обратно.
Сначала останавливаем proxy и сервер модели:
systemctl stop llm-proxy
systemctl stop llama-server
Новую модель лучше скачать в отдельную папку. Например, так:
mkdir -p /root/qwen25
После загрузки новой модели открываем systemd-сервис:
nano /etc/systemd/system/llama-server.service
Внутри нужно заменить только строку ExecStart.
Например, раньше там была модель Qwen3:
ExecStart=/root/llama/llama.cpp/build/bin/llama-server -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf -t 1 -c 1024 --host 127.0.0.1 --port 8093
После смены модели строка будет указывать уже на новый GGUF-файл:
ExecStart=/root/llama/llama.cpp/build/bin/llama-server -m /root/qwen25/ИМЯ_МОДЕЛИ.gguf -t 1 -c 1024 --host 127.0.0.1 --port 8093
Теперь перечитываем systemd-конфиги и запускаем сервисы обратно:
systemctl daemon-reload
systemctl start llama-server
systemctl start llm-proxy
Проверяем, что всё поднялось:
systemctl status llama-server
systemctl status llm-proxy
ss -tulpn | grep 809
curl http://127.0.0.1:8093/health
curl http://127.0.0.1:8092/health
Старую модель лучше удалять только после успешной проверки новой. Если новая модель запустилась нормально, папку Qwen3 можно убрать:
rm -rf /root/qwen3
После такой замены внешний API остаётся прежним:
http://VPS_IP:8092/generate
Токен тоже остаётся прежним. Код сайта или Telegram-бота менять не нужно. Меняется только модель, которая стоит за внутренним llama-server.
Итоги теста Qwen3-1.7B на слабом VPS
Модель удалось запустить на VPS с 1 CPU и 2 ГБ RAM. API через proxy тоже работает. Но практические тесты показали важное ограничение: для автоматических задач модель оказалась нестабильной.
Главная проблема — модель часто не выполняет запрос напрямую. Вместо ответа она начинает рассуждать о задании, повторять части инструкции или придумывать новые данные. Иногда генерация уходит в длинный повтор и не останавливается.
Пример 1. Простая сказка
Запрос был простой: придумать короткую добрую сказку со свинкой, волком и жирафом. Но вместо сказки модель начала повторять техническое условие.
PROMPT:
Придумай короткую интересную сказку для ребёнка.
Персонажи: свинка, волк, жираф.
Сказка должна быть доброй.
Не больше 120 слов.
RESPONSE:
Нужно, чтобы в сказке была погода, которая меняется,
и в конце она становится хорошей.
Нужно, чтобы в сказке была погода, которая меняется,
и в конце она становится хорошей.
Нужно, чтобы в сказке была погода, которая меняется,
и в конце она становится хорошей...
Пример 2. JSON без лишнего текста
Второй тест проверял строгий формат. Нужно было убрать дубликаты из списка фруктов, отсортировать их и вернуть только JSON. Модель добавила фрукты, которых не было во входных данных.
PROMPT:
У тебя есть список: яблоко, банан, яблоко, груша, банан, слива.
Верни JSON.
Нужно убрать дубликаты и отсортировать по алфавиту.
Ответ только JSON.
RESPONSE:
Никаких других текстовых объяснений.
```json
["абрикос", "ананас", "апельсин", "бальзамик", "бергамот",
"клубника", "киви", "лимон", "мандарины", "малина",
"огурец", "персик", "яблоко", "банан", "груша", "слива"]
```
Для автоматической обработки это опасно. Такой ответ нельзя надёжно парсить. Кроме того, модель нарушила главное правило: не добавлять данные от себя.
Пример 3. Классификация сообщений
Третий тест был ближе к игровой задаче. Нужно было разделить сообщения на игровые действия и обычный чат между игроками. Модель вместо классификации начала дописывать новые сообщения.
PROMPT:
Классифицируй сообщения.
Ответ только JSON-массив.
Категории: game, chat.
Сообщения:
1. открываю сундук
2. ребята идем налево
3. атакую волка
4. ахаха это было смешно
5. осматриваю комнату
6. давайте сначала отдохнем
RESPONSE:
7. пойдем в магазин
8. открыли сундук
9. атакуем волка
10. пойдем в магазин
11. открыли сундук
12. атакуем волка
13. ахаха это было смешно
14. осматриваем комнату...
Именно поэтому я не стал использовать эту модель для маршрутизации голосовых сообщений в игре. Если модель ошибётся, сообщение уйдёт не туда: либо всем игрокам, либо мастеру. Для такой задачи надёжнее поставить две кнопки выбора.
Что можно поручить такой модели
Qwen3-1.7B в таком запуске не стоит использовать как полноценного игрового мастера или строгий JSON-генератор. Она может пригодиться только там, где ошибка не ломает механику игры.
- черновики коротких описаний;
- варианты названий;
- простые идеи для NPC;
- переформулировка одной короткой фразы;
- локальные эксперименты без влияния на игроков.
Для строгих задач модель пока не подходит. Ей нельзя доверять классификацию сообщений, генерацию JSON для движка, разбор голосовых команд и важные игровые решения. Там лучше использовать более сильную модель или оставлять выбор за игроком.
Вывод
Qwen3-1.7B можно запустить на очень слабом VPS. Но сам факт запуска ещё не значит, что модель подходит для полезной автоматизации. На 1 CPU и 2 ГБ RAM она скорее демонстрирует возможность, чем даёт стабильный рабочий инструмент.