umbot
    Preparing search index...

    Deploying to production

    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.

    • A server with a public IP address
    • A domain name
    • An SSL certificate (required for Alice, Sber SmartApp, Telegram, MAX, Viber and other platforms)
    curl https://get.acme.sh | sh
    
    acme.sh --issue -d example.com -w /var/www/example
    

    Where:

    • example.com — your domain
    • /var/www/example — the site root directory (it must be reachable over HTTP to pass the check)
    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:

    • Yandex Dialogs
    • Sber SmartApp (developers.sber.ru)
    • Marusia for developers
    • Telegram BotFather, VK Callback API, Viber Bot Settings, etc.
    const server = bot.start('0.0.0.0', 3000);
    
    • GET /health → { status: 'ok', timestamp } (200)
    • POST / → webhookHandle (handles the platform request)
    • Maximum body size: 2 MB — with a valid Content-Length above the limit, 413 is returned; with a streamed body above the limit, the excess is dropped and the request fails with 500
    • SIGTERM/SIGINT → graceful shutdown (close() + 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:

    • Building the project,
    • Building the Docker image,
    • Deploying to the server over SSH.

    🔐 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:

    • A handler export in src/index.ts for handling Cloud Functions requests
    • scripts/deploy.js — deployment via the yc CLI (run with npm run deploy)
    • A reference 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.json

    Setting 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: true reliably stores data only on Alice, SmartApp and Marusia (the state arrives in the request). On Telegram/VK/MAX/Viber without a DB adapter, userData lives 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:

    • an external logger: bot.setLogger({ log, warn, error }) sending to your logging system (or collect stdout — the cloud collects it itself);
    • or keep the default behavior: if a log cannot be written, the framework does not loop, but writes a single message to stderr and pauses write attempts for a minute.

    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:

    • [ ] The build succeeded — npm run build with no errors
    • [ ] Tests pass — npm run test is green
    • [ ] strict_prod mode — bot.setAppMode('strict_prod') or NODE_ENV=production (without an explicit setAppMode)
    • [ ] Webhook signature verification is enabled — 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 verification
    • [ ] npx umbot doctor reports no errors — tokens work, webhooks are registered
    • [ ] Tokens are in environment variables — not in the code, not in a .env inside the container
    • [ ] MongoAdapter — instead of FileAdapter (FileAdapter keeps data in memory)
    • [ ] HTTPS is set up — required for Alice, Sber, Viber
    • [ ] The webhook URL is registered — in each platform's developer console
    • [ ] error_log is configured — bot.setAppConfig({ error_log: './logs' })
    • [ ] Preload is done — all media files are preloaded
    • [ ] rateLimiter is connected — bot.use(rateLimiter())
    • [ ] re2 is installed — npm install re2: regular expressions without catastrophic backtracking
    • [ ] PM2 or Docker — for automatic restarts on crashes
    • [ ] Monitoring — logs are accessible, metrics are set up