This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.
Once a voice skill or chatbot is written and tested locally, you need to deploy it to a server so that platforms can send requests to it. This guide covers the full deployment cycle: from getting an SSL certificate to setting up CI/CD.
acme.sh:curl https://get.acme.sh | sh
acme.sh --issue -d example.com -w /var/www/example
Where:
acme.sh --install-cert -d example.com \
--key-file /etc/ssl/private/example.key \
--fullchain-file /etc/ssl/certs/example.crt \
--reloadcmd "sudo systemctl reload nginx"
Add to the nginx configuration:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
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;
}
}
Reload nginx:
sudo systemctl reload nginx
Build the project:
npm run build
Run it with pm2 (recommended for production). If pm2 is not installed, install it:
npm install -g pm2
Start the process itself:
pm2 start dist/index.js --name "umbot-production"
To keep the application available after a server reboot, run the following commands:
pm2 startup
pm2 save
Your skill is now available over HTTPS and ready to be connected in the developer consoles:
const server = bot.start('0.0.0.0', 3000);
/health → { status: 'ok', timestamp } (200)/ → webhookHandle (handles the platform request)Content-Length above the limit, 413 is returned;
with a streamed body above the limit, the excess is dropped and the request fails with 500close() + cleanup + process.exit(0))See the Universal webhook handler section of the platform guide.
process.on('SIGTERM', async () => {
console.log('Shutting down...');
await bot.close(); // stops the server, saves data, closes the database
process.exit(0);
});
bot.start() already installs these hooks automatically — you usually do not need to do it yourself.
When a project is created with the CLI --prod flag, a ready-made Dockerfile is generated.
Build the image and run the container, passing tokens through environment variables:
docker build -t my-bot .
docker run -p 3000:3000 --env-file .env my-bot
# or individually:
docker run -p 3000:3000 -e ALISA_TOKEN=... -e TELEGRAM_TOKEN=... my-bot
The image is built in two stages (building TypeScript with dev dependencies, then a runtime with production dependencies only),
sets NODE_ENV=production — without an explicit setAppMode() the bot runs in strict_prod — and contains no .env:
tokens are passed at startup.
The process in the container runs as the unprivileged umbot user; the /app/json (FileAdapter
data) and /app/logs (file logs) directories are created in the image in advance and are writable by it. Container data
is lost when the container is recreated: for FileAdapter mount a volume (docker run -v umbot-data:/app/json ...), and for
production use MongoAdapter. Without a custom logger, errors are duplicated to stderr ([umbot] ...) and visible in
docker logs.
If env is not configured, the framework silently picks up the known variables (TELEGRAM_TOKEN,
ALISA_TOKEN, VK_TOKEN, ...) from the container environment and fills in the tokens with them — you do not need to write
env: 'local' for this. If env: 'local' is set, the values from the environment
overwrite the configured tokens.
The .github/workflows/deploy.yml template automatically sets up:
🔐 Security: never commit .env to Git. Use GitHub Secrets.
For platforms without a permanent server (Alice, SmartApp, Marusia) you can use serverless functions.
When creating a project with the CLI, you can automatically generate a Yandex Cloud Functions configuration:
npx umbot create from-flow flow.json --usecloud
This adds to the project:
handler export in src/index.ts for handling Cloud Functions requestsscripts/deploy.js — deployment via the yc CLI (run with npm run deploy)serverless.yml with the function configuration (the deployment does not read it — scripts/deploy.js builds the yc arguments)scripts/deploy.js reads .env by the same rules as the framework (an inline comment starts only with " #", outer
quotes are removed, empty values are skipped) and passes the values to --environment. Such variables are stored
in the function version in plain text and are visible to anyone with access to the function in the cloud console. For production, keep
tokens in Yandex Lockbox and attach the secret to the function (yc serverless function version create ... --secret ...),
removing them from .env.
deploy and build scripts in package.jsonSetting up a Cloud Function manually:
import { Bot } from 'umbot';
import { fullPlatforms } from 'umbot/plugins';
const bot = new Bot();
bot.use(fullPlatforms);
bot.setAppConfig({ isLocalStorage: true });
// Function export for Yandex Cloud Functions
export const handler = async (event: Record<string, unknown>) => {
const rawBody = typeof event.body === 'string' ? event.body : '';
// A body with a non-JSON Content-Type arrives in base64 (isBase64Encoded: true)
const content =
typeof event.body !== 'string'
? JSON.stringify(event.body ?? '')
: event.isBase64Encoded === true
? Buffer.from(rawBody, 'base64').toString('utf8')
: rawBody;
const headers = (event.headers ?? {}) as Record<string, unknown>;
// Client IP — for the ipFilter middleware
const requestContext = event.requestContext as { identity?: { sourceIp?: string } } | undefined;
const result = await bot.webhookEvent(content, headers, requestContext?.identity?.sourceIp);
return {
statusCode: result.statusCode,
headers: { 'Content-Type': 'application/json' },
body: typeof result.body === 'string' ? result.body : JSON.stringify(result.body ?? ''),
};
};
webhookEvent() is a dedicated method for serverless environments: unlike run(), it
detects the platform from the content itself, verifies the webhook signature (isCorrectQuery) and returns
a ready HTTP response { statusCode, body }. This is exactly the code the from-flow --usecloud generator uses.
Header name case does not matter: Cloud Functions passes them as the client sent them
(X-Telegram-Bot-Api-Secret-Token), and webhookEvent() lowercases them before verifying the signature.
In serverless,
isLocalStorage: truereliably stores data only on Alice, SmartApp and Marusia (the state arrives in the request). On Telegram/VK/MAX/Viber without a DB adapter,userDatalives in the memory of the function instance and is lost when a call lands on a new instance — for dialog steps on chat platforms, connect a database. In Cloud Functions the most convenient option is umbot-ydb-adapter: YDB in Serverless mode, access through the function's service account without passwords, and the adapter creates the tables itself.
Ready-made recipes are in the Recipes section.
One Bot instance is one Node.js process. Webhook handling does not depend on the process as long as the data lives outside
it: userData is read from the database, and the state of voice platforms (Alice, SmartApp, Marusia) arrives in the body of the
request itself. So a bot with a database scales horizontally without extra mechanisms (sessions, sticky load balancing).
Only the memorySession (Telegram, VK, MAX, Viber without a database) and the request queue of a single
user live in process memory: with several instances, connect a database; the order of a single user's requests is guaranteed only
within one instance.
The framework's per-request overhead is small (numbers are in Performance and guarantees). In practice the bottleneck is almost never the framework itself but external calls (the database, platform APIs). If the bot runs on a single server and the load is moderate, one process is enough, and it is the simplest configuration.
When you need fault tolerance and load distribution, run N identical instances (Docker replicas, PM2 cluster, several servers behind nginx):
# PM2 cluster: one process per core
pm2 start dist/index.js -i max
# or Docker Compose
# deploy:
# replicas: 4
The only hard requirement is a shared database:
| Configuration | Multiple processes |
|---|---|
MongoAdapter, umbot-knex-adapter, umbot-ydb-adapter or your own |
✅ Yes |
FileAdapter |
❌ No |
FileAdapter reads and writes JSON files without locks and is designed strictly for a single
process — this is a documented limitation, not a bug. With several processes,
each instance sees its own copy of the data, and writes start getting lost. So
in multi-process and multi-server configurations use MongoAdapter, the external adapters
umbot-knex-adapter (PostgreSQL, MySQL, SQLite) and
umbot-ydb-adapter (YDB), or your own
(see DB adapters).
Media token caches (ImageTokens, SoundTokens) are stored in the same database, so
all instances share the tokens and do not re-upload images and sounds
again.
If a failure of one platform must not take down the others (for example, an internal
chatbot and an Alice skill have different SLAs), deploy a separate process per platform:
its own Bot instance with its own set of adapters and its own webhook URL. Move the business logic
(commands, steps, middleware) into a shared module and connect it in both processes —
it is ordinary code that is not tied to a Bot instance.
// process-telegram.ts
const bot = new Bot().use(new TelegramAdapter(process.env.TELEGRAM_TOKEN!));
applySharedLogic(bot); // shared commands and steps from your module
bot.start('0.0.0.0', 3001);
// process-alisa.ts
const bot = new Bot().use(new AlisaAdapter());
applySharedLogic(bot);
bot.start('0.0.0.0', 3002);
Pros: a crash or restart of one process does not affect the others, and the limits and load of each platform are visible separately. Cons: more processes mean more infrastructure (ports, health checks, deployment).
In serverless environments the file system is read-only, so
file logs (error_log) cannot be written there — and you do not need them. Set up one of these
options:
bot.setLogger({ log, warn, error }) sending to
your logging system (or collect stdout — the cloud collects it
itself);Serverless also means cold starts and a short process lifetime —
FileAdapter is unsuitable there for the same reason: the data must live in an external database.
Before going to production, make sure that:
npm run build with no errorsnpm run test is greenstrict_prod mode — bot.setAppMode('strict_prod') or NODE_ENV=production (without an explicit setAppMode)TELEGRAM_WEBHOOK_SECRET / MAX_WEBHOOK_SECRET / VK_SECRET_KEY is set;
on start, the log has no warning about a webhook without signature verification (Alice, SmartApp and Marusia have no signature —
do not treat their userId as an authenticated identity). More in Configuration → Webhook signature
verificationnpx umbot doctor reports no errors — tokens work, webhooks are registeredbot.setAppConfig({ error_log: './logs' })bot.use(rateLimiter())npm install re2: regular expressions without catastrophic backtrackingFull reference — API v-3.1 · all versions.