<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>Yuriy Gavrilov: posts tagged k3s</title>
<link>https://gavrilov.info/tags/k3s/</link>
<description>Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov</description>
<author></author>
<language>en</language>
<generator>Aegea 11.4 (v4171e)</generator>

<itunes:owner>
<itunes:name></itunes:name>
<itunes:email>yvgavrilov@gmail.com</itunes:email>
</itunes:owner>
<itunes:subtitle>Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov</itunes:subtitle>
<itunes:image href="https://gavrilov.info/pictures/userpic/userpic-square@2x.jpg?1643451008" />
<itunes:explicit>no</itunes:explicit>

<item>
<title>Сравнительный анализ self-hosted S3-совместимых хранилищ</title>
<guid isPermaLink="false">299</guid>
<link>https://gavrilov.info/all/sravnitelny-analiz-self-hosted-s3-sovmestimyh-hranilisch/</link>
<pubDate>Mon, 08 Dec 2025 00:50:17 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/sravnitelny-analiz-self-hosted-s3-sovmestimyh-hranilisch/</comments>
<description>
&lt;p&gt;Четкое сравнение семи self-hosted S3-совместимых решений для хранения данных.&lt;/p&gt;
&lt;p&gt;Оригинал тут: &lt;a href="https://www.repoflow.io/blog/benchmarking-self-hosted-s3-compatible-storage-a-practical-performance-comparison"&gt;Команда RepoFlow. 9 августа 2025 г.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Локальное (self-hosted) объектное хранилище — это отличный выбор для разработчиков и команд, которые хотят иметь полный контроль над хранением и доступом к своим данным. Независимо от того, заменяете ли вы Amazon S3, размещаете внутренние файлы, создаете CI-конвейер или обслуживаете репозитории пакетов, уровень хранения может значительно повлиять на скорость и стабильность.&lt;/p&gt;
&lt;p&gt;Мы протестировали семь популярных решений для объектного хранения, поддерживающих протокол S3. Цель состояла в том, чтобы сравнить их производительность в идентичных условиях, используя реальные операции загрузки и скачивания.&lt;/p&gt;
&lt;h3&gt;Тестируемые решения&lt;/h3&gt;
&lt;p&gt;Каждое из следующих решений было развернуто с помощью Docker на одном и том же сервере без монтирования томов и без специальной настройки:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;`MinIO`&lt;/li&gt;
&lt;li&gt;`Ceph`&lt;/li&gt;
&lt;li&gt;`SeaweedFS`&lt;/li&gt;
&lt;li&gt;`Garage`&lt;/li&gt;
&lt;li&gt;`Zenko` (Scality Cloudserver)&lt;/li&gt;
&lt;li&gt;`LocalStack`&lt;/li&gt;
&lt;li&gt;`RustFS`&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Скорость последовательного скачивания&lt;/h3&gt;
&lt;p&gt;Средняя скорость скачивания одного файла разного размера.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.42.26.png" width="1270" height="656" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости последовательного скачивания для малых файлов размером 50 КБ и 200 КБ. По оси Y — скорость в МБ/с, по оси X — размер файла. Сравниваются Garage, Localstack, Minio, Zenko, Ceph, RustFS, SeaweedFS.]&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.42.57.png" width="1264" height="650" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости последовательного скачивания для больших файлов размером 10 МБ, 50 МБ, 100 МБ и 1 ГБ. По оси Y — скорость в МБ/с, по оси X — размер файла. Сравниваются те же решения.]&lt;/p&gt;
&lt;h3&gt;Скорость последовательной загрузки&lt;/h3&gt;
&lt;p&gt;Средняя скорость загрузки одного файла разного размера.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.43.18.png" width="1278" height="648" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости последовательной загрузки для малых файлов размером 50 КБ и 200 КБ. По оси Y — скорость в МБ/с, по оси X — размер файла. Сравниваются те же решения.]&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.43.34.png" width="1252" height="636" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости последовательной загрузки для больших файлов размером 10 МБ, 50 МБ, 100 МБ и 1 ГБ. По оси Y — скорость в МБ/с, по оси X — размер файла. Сравниваются те же решения.]&lt;/p&gt;
&lt;h3&gt;Производительность листинга&lt;/h3&gt;
&lt;p&gt;Измеряет время, необходимое для получения списка всех 2000 тестовых объектов в бакете с использованием разных размеров страницы (100, 500 и 1000 результатов на запрос).&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.44.02.png" width="1280" height="648" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График производительности листинга. По оси Y — время в мс, по оси X — количество результатов на страницу (100, 500, 1000). Сравниваются те же решения.]&lt;/p&gt;
&lt;h3&gt;Скорость параллельной загрузки&lt;/h3&gt;
&lt;p&gt;Измеряет время, необходимое для параллельной загрузки нескольких файлов одинакового размера. Скорость загрузки рассчитывается по формуле:&lt;/p&gt;
&lt;p&gt;(number of files × file size) ÷ total time&lt;/p&gt;
&lt;h4&gt;Скорость параллельной загрузки – файлы 1 МБ&lt;/h4&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.44.22.png" width="1304" height="748" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости параллельной загрузки файлов размером 1 МБ. По оси Y — скорость в МБ/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]&lt;/p&gt;
&lt;h4&gt;Скорость параллельной загрузки – файлы 10 МБ&lt;/h4&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.45.06.png" width="1254" height="650" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости параллельной загрузки файлов размером 10 МБ. По оси Y — скорость в МБ/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]&lt;/p&gt;
&lt;h4&gt;Скорость параллельной загрузки – файлы 100 МБ&lt;/h4&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.45.23.png" width="1278" height="652" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости параллельной загрузки файлов размером 100 МБ. По оси Y — скорость в МБ/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]&lt;/p&gt;
&lt;h3&gt;Скорость параллельного скачивания&lt;/h3&gt;
&lt;p&gt;Измеряет время, необходимое для параллельного скачивания нескольких файлов одинакового размера. Скорость скачивания рассчитывается по формуле:&lt;/p&gt;
&lt;p&gt;(number of files × file size) ÷ total time&lt;/p&gt;
&lt;h4&gt;Скорость параллельного скачивания – файлы 1 МБ&lt;/h4&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.45.45.png" width="1278" height="650" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости параллельного скачивания файлов размером 1 МБ. По оси Y — скорость в МБ/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]&lt;/p&gt;
&lt;h4&gt;Скорость параллельного скачивания – файлы 10 МБ&lt;/h4&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.46.02.png" width="1280" height="644" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости параллельного скачивания файлов размером 10 МБ. По оси Y — скорость в МБ/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]&lt;/p&gt;
&lt;h4&gt;Скорость параллельного скачивания – файлы 100 МБ&lt;/h4&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-00.46.17.png" width="1284" height="670" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;[Изображение: График скорости параллельного скачивания файлов размером 100 МБ. По оси Y — скорость в МБ/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]&lt;/p&gt;
&lt;h3&gt;Как проводились тесты&lt;/h3&gt;
&lt;p&gt;Для каждого решения мы:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Загружали и скачивали файлы 7 различных размеров: 50 КБ, 200 КБ, 1 МБ, 10 МБ, 50 МБ, 100 МБ и 1 ГБ.&lt;/li&gt;
&lt;li&gt;Повторяли каждую загрузку и скачивание 20 раз для получения стабильных средних значений.&lt;/li&gt;
&lt;li&gt;Измеряли среднюю скорость загрузки и скачивания в мегабайтах в секунду (МБ/с).&lt;/li&gt;
&lt;li&gt;Выполняли все тесты на одной и той же машине, используя стандартный Docker-контейнер для каждой системы хранения, без внешних томов, монтирования или кешей.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Все решения тестировались в одноузловой конфигурации для обеспечения согласованности. Хотя некоторые системы (например, `Ceph`) спроектированы для лучшей производительности в кластерной среде, мы использовали одинаковые условия для всех решений, чтобы гарантировать справедливое сравнение.&lt;/p&gt;
&lt;h3&gt;Заключительные мысли&lt;/h3&gt;
&lt;p&gt;Эти результаты показывают, как каждое решение вело себя в нашей конкретной тестовой среде с одним узлом. Их следует рассматривать как относительное сравнение соотношений производительности, а не как абсолютные жесткие значения, которые будут применимы в любой конфигурации.&lt;/p&gt;
&lt;p&gt;При выборе подходящего решения для хранения данных учитывайте типичные размеры файлов, которые вы будете хранить, поскольку одни системы лучше справляются с маленькими файлами, а другие преуспевают с большими. Также подумайте об основных возможностях, которые вам требуются, таких как масштабируемость, репликация, долговечность или встроенный графический интерфейс. Наконец, помните, что производительность может сильно отличаться между одноузловыми и многоузловыми кластерами.&lt;/p&gt;
&lt;p&gt;Наши тесты предоставляют базовый уровень для понимания того, как эти системы соотносятся в идентичных условиях, но ваша реальная производительность будет зависеть от вашего конкретного оборудования, рабочей нагрузки и конфигурации.&lt;/p&gt;
</description>
</item>

<item>
<title>Создание масштабируемого сервиса для скриншотов сайтов на Node.js, Puppeteer и Kubernetes</title>
<guid isPermaLink="false">297</guid>
<link>https://gavrilov.info/all/sozdanie-masshtabiruemogo-servisa-dlya-skrinshotov-saytov-na-nod/</link>
<pubDate>Mon, 01 Dec 2025 00:28:00 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/sozdanie-masshtabiruemogo-servisa-dlya-skrinshotov-saytov-na-nod/</comments>
<description>
&lt;p&gt;В современном вебе часто возникает задача автоматического создания скриншотов веб-страниц. Это может быть нужно для генерации превью ссылок, мониторинга доступности сайтов, создания PDF-отчетов или даже для работы сервисов по автоматизации контента.&lt;/p&gt;
&lt;p&gt;В этой статье мы пошагово создадим свой собственный, надежный и масштабируемый микросервис для рендеринга скриншотов. Мы будем использовать мощную связку технологий:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Node.js &amp; Express:&lt;/b&gt; для создания легковесного API.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Puppeteer:&lt;/b&gt; для управления headless-браузером Chromium и создания скриншотов.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker:&lt;/b&gt; для упаковки нашего приложения в изолированный контейнер.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kubernetes (k3s):&lt;/b&gt; для оркестрации, масштабирования и обеспечения отказоустойчивости нашего сервиса.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;В итоге мы получим простое API, которое по GET-запросу с URL-адресом будет возвращать готовую картинку.&lt;/p&gt;
&lt;h3&gt;Зачем нужен собственный сервис?&lt;/h3&gt;
&lt;p&gt;Хотя существуют готовые SaaS-решения вроде &lt;a href="https://urlbox.com,"&gt;https://urlbox.com,&lt;/a&gt; создание собственного сервиса дает несколько ключевых преимуществ:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Контроль:&lt;/b&gt; Вы полностью контролируете окружение, версии браузера и параметры рендеринга.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Безопасность:&lt;/b&gt; Вы можете запускать сервис в своей приватной сети для обработки внутренних ресурсов, недоступных извне.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Стоимость:&lt;/b&gt; При большом объеме запросов собственный сервис может оказаться значительно дешевле SaaS.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Гибкость:&lt;/b&gt; Вы можете легко расширить функциональность, добавив, например, генерацию PDF, выполнение кастомных скриптов на странице и многое другое.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Шаг 1: Создание Node.js приложения&lt;/h3&gt;
&lt;p&gt;Сначала создадим основу нашего сервиса — Express-приложение, которое будет принимать запросы и управлять Puppeteer.&lt;/p&gt;
&lt;h4&gt;Структура проекта&lt;/h4&gt;
&lt;p&gt;Создадим папку для нашего проекта и внутри нее — необходимые файлы.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;puppeteer-k8s/
├── dockerb/
│   ├── Dockerfile
│   ├── .dockerignore
│   ├── server.js
│   ├── package.json
│   └── package-lock.json
└── deployment.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;*Обратите внимание: мы поместили все, что относится к Docker, в подпапку `dockerb` для удобства.*&lt;/p&gt;
&lt;h4&gt;Код сервера (`server.js`)&lt;/h4&gt;
&lt;p&gt;Это ядро нашего приложения. Оно принимает HTTP-запросы, запускает Puppeteer, переходит на указанный URL и делает скриншот. Мы также добавим полезные параметры для управления качеством и размером.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;// server.js. -- в моем докере server_v3.js

const express = require('express');
const puppeteer = require('puppeteer');

const app = express();
const PORT = 3000;

app.get('/render', async (req, res) =&amp;gt; {
    // Добавляем новые параметры: quality и dsf (deviceScaleFactor)
    const { 
        url, 
        width, 
        height, 
        format = 'png', 
        fullPage = 'false',
        quality, // Качество для jpeg/webp (0-100)
        dsf = '1' // Device Scale Factor. Для Retina-качества используйте '2'
    } = req.query;

    if (!url) {
        return res.status(400).json({
            error: 'Параметр &amp;quot;url&amp;quot; обязателен.',
            example: '/render?url=https://example.com'
        });
    }

    let browser;
    try {
        console.log(`[${new Date().toISOString()}] Начинаем рендеринг для: ${url}`);
        
        // Добавляем аргументы для улучшения рендеринга
        browser = await puppeteer.launch({
            args: [
                '--no-sandbox',
                '--disable-setuid-sandbox',
                '--font-render-hinting=none', // Может улучшить отрисовку шрифтов
                '--disable-infobars'       // Убирает инфо-панели
            ]
        });
        
        const page = await browser.newPage();

        const viewportWidth = parseInt(width, 10) || 1280;
        const viewportHeight = parseInt(height, 10) || 720;
        const deviceScaleFactor = parseInt(dsf, 10) || 1;

        // Устанавливаем viewport с учетом deviceScaleFactor
        await page.setViewport({
            width: viewportWidth,
            height: viewportHeight,
            deviceScaleFactor: deviceScaleFactor
        });

        console.log(`Viewport: ${viewportWidth}x${viewportHeight}, DeviceScaleFactor: ${deviceScaleFactor}`);

        await page.goto(url, { waitUntil: 'networkidle2' });

        // Если есть &amp;quot;ленивая&amp;quot; загрузка изображений, можно &amp;quot;проскроллить&amp;quot; страницу
        // Это необязательный шаг, но он помогает прогрузить все картинки
        if (fullPage === 'true') {
             await page.evaluate(async () =&amp;gt; {
                await new Promise((resolve) =&amp;gt; {
                    let totalHeight = 0;
                    const distance = 100;
                    const timer = setInterval(() =&amp;gt; {
                        const scrollHeight = document.body.scrollHeight;
                        window.scrollBy(0, distance);
                        totalHeight += distance;

                        if (totalHeight &amp;gt;= scrollHeight) {
                            clearInterval(timer);
                            resolve();
                        }
                    }, 100);
                });
            });
        }
        
        const screenshotOptions = {
            type: (format === 'jpeg' || format === 'webp') ? format : 'png',
            fullPage: fullPage === 'true'
        };

        // Устанавливаем качество, если параметр был передан
        if ((screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'webp') &amp;amp;&amp;amp; quality) {
            screenshotOptions.quality = parseInt(quality, 10);
        } else if (screenshotOptions.type === 'jpeg') {
            screenshotOptions.quality = 90; // Ставим хорошее качество по умолчанию для JPEG
        }

        const imageBuffer = await page.screenshot(screenshotOptions);
        
        console.log(`[${new Date().toISOString()}] Рендеринг успешно завершен.`);

        res.setHeader('Content-Type', `image/${screenshotOptions.type}`);
        res.send(imageBuffer);

    } catch (error) {
        console.error(`[${new Date().toISOString()}] Ошибка рендеринга для ${url}:`, error.message);
        res.status(500).json({ 
            error: 'Не удалось отрендерить страницу.',
            details: error.message 
        });
    } finally {
        if (browser) {
            await browser.close();
        }
    }
});


app.get('/', (req, res) =&amp;gt; {
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.send(`
    API для рендеринга сайтов (v2 - Улучшенное качество)
    ----------------------------------------------------
    Используйте GET-запрос на /render с параметрами.

    Обязательные параметры:
      - url: Адрес сайта для скриншота.

    Необязательные параметры:
      - width: Ширина окна (по умолч. 1280).
      - height: Высота окна (по умолч. 720).
      - format: Формат файла ('png', 'jpeg', 'webp'). По умолч. 'png'.
      - fullPage: Сделать скриншот всей страницы ('true' или 'false'). По умолч. 'false'.
      - dsf: Device Scale Factor для Retina/HiDPI качества (например, '2'). По умолч. '1'.
      - quality: Качество для jpeg/webp от 0 до 100.

    Пример для высокого качества (Retina):
    curl -o site_retina.png &amp;quot;http://localhost:3000/render?url=https://github.com&amp;amp;dsf=2&amp;amp;width=1440&amp;amp;height=900&amp;quot;
    
    Пример для JPEG с максимальным качеством:
    curl -o site_hq.jpeg &amp;quot;http://localhost:3000/render?url=https://apple.com&amp;amp;format=jpeg&amp;amp;quality=100&amp;amp;width=1920&amp;amp;height=1080&amp;quot;
    `);
});

app.listen(PORT, () =&amp;gt; {
    console.log(`Сервис рендеринга запущен на http://localhost:${PORT}`);
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;*Запустите `npm install` в папке `dockerb`, чтобы сгенерировать `package-lock.json`.*&lt;/p&gt;
&lt;h3&gt;Шаг 2: Создание Docker-образа&lt;/h3&gt;
&lt;p&gt;Теперь упакуем наше приложение. Этот шаг — самый каверзный, так как Puppeteer требует особого подхода к правам доступа и зависимостям.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hub.docker.com/repository/docker/1325gy/my_dev/tags/v4"&gt;мой пример тут образа &lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Файл `.dockerignore`&lt;/h4&gt;
&lt;p&gt;Крайне важно не копировать локальную папку `node_modules` в образ. Они должны устанавливаться внутри контейнера для правильной архитектуры.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# .dockerignore
node_modules&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Финальный `Dockerfile`&lt;/h4&gt;
&lt;p&gt;Этот `Dockerfile` — результат множества проб и ошибок. Он решает все основные проблемы:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Устанавливает необходимые системные библиотеки для Chromium.&lt;/li&gt;
&lt;li&gt;Создает непривилегированного пользователя `pptruser` с домашней директорией.&lt;/li&gt;
&lt;li&gt;Устанавливает `npm`-зависимости и браузер от имени этого пользователя, чтобы избежать проблем с правами доступа.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# 1. Базовый образ
FROM node:18-slim

# 2. Установка системных зависимостей
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \
    libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \
    libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 \
    libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
    libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 \
    libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# 3. создаем пользователя с домом
# Флаг -m (--create-home) гарантирует создание папки /home/pptruser
RUN useradd -r -m -g root -s /bin/bash pptruser

# 4. Устанавливаем рабочую директорию
WORKDIR /app

# 5. Копируем файлы зависимостей и сразу назначаем владельцем pptruser
# (современный и более чистый способ, чем chown после)
COPY --chown=pptruser:root package*.json ./

# 6. Переключаемся на нашего пользователя
USER pptruser

# 7. Устанавливаем всё от имени pptruser. Кэш ляжет в /home/pptruser/.cache
RUN npm ci
RUN npx puppeteer browsers install chrome

# 8. Копируем остальной код, также сразу назначая владельца
COPY --chown=pptruser:root . .

# 9. Открываем порт и запускаем
EXPOSE 3000
CMD [ &amp;quot;node&amp;quot;, &amp;quot;server.js&amp;quot; ]&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Сборка и отправка образа&lt;/h4&gt;
&lt;p&gt;Замените `your_docker_id` на ваш логин в Docker Hub.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# Переходим в папку с Dockerfile
cd puppeteer-k8s/dockerb

# Собираем образ
docker build -t your_docker_id/puppeteer-renderer:v1 .

# Отправляем в репозиторий
docker push your_docker_id/puppeteer-renderer:v1&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Шаг 3: Развертывание в Kubernetes&lt;/h3&gt;
&lt;p&gt;Мы будем использовать `k3s` — легковесный дистрибутив Kubernetes.  Можно еще тут посмотреть пример &lt;a href="https://github.com/jamesheronwalker/urlbox-puppeteer-kubernetes-demo"&gt;https://github.com/jamesheronwalker/urlbox-puppeteer-kubernetes-demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;или вот статейка еще &lt;a href="https://urlbox.com/guides/kubernetes-website-screenshots"&gt;https://urlbox.com/guides/kubernetes-website-screenshots&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Установка окружения (Local)&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# Установка lima и k3s (если еще не сделано)
brew install lima
limactl start template://k3s

# Настройка kubectl для работы с кластером
export KUBECONFIG=$(limactl list k3s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')

# Проверка, что все работает
kubectl get nodes&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Манифест развертывания (`deployment.yaml`)&lt;/h4&gt;
&lt;p&gt;Этот YAML-файл описывает желаемое состояние нашего приложения в кластере. Мы будем использовать тип сервиса `LoadBalancer` — он идеально подходит для будущего развертывания в облаке, а для локальной разработки мы будем использовать `port-forward`.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: puppeteer-renderer
spec:
  replicas: 2 # Запускаем два пода для отказоустойчивости
  selector:
    matchLabels:
      app: puppeteer-renderer
  template:
    metadata:
      labels:
        app: puppeteer-renderer
    spec:
      containers:
      - name: renderer
        image: your_docker_id/puppeteer-renderer:v1 # &amp;lt;&amp;lt;&amp;lt; ВАШ ОБРАЗ (мой тут docker pull 1325gy/my_dev:v4)
        ports:
        - containerPort: 3000
        resources:
          requests:
            cpu: &amp;quot;500m&amp;quot;
            memory: &amp;quot;1Gi&amp;quot;
          limits:
            cpu: &amp;quot;1&amp;quot;
            memory: &amp;quot;2Gi&amp;quot;
        readinessProbe:
          httpGet:
            path: /
            port: 3000
          initialDelaySeconds: 15
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 20
        volumeMounts:
        - name: dshm
          mountPath: /dev/shm
      volumes:
      - name: dshm
        emptyDir:
          medium: Memory
---
apiVersion: v1
kind: Service
metadata:
  name: pptr-renderer-service
spec:
  # Тип LoadBalancer - стандарт для облаков. Локально он останется в &amp;lt;pending&amp;gt;,
  # но это не помешает нам использовать port-forward.
  type: LoadBalancer 
  selector:
    app: puppeteer-renderer
  ports:
  - protocol: TCP
    port: 80 # Внешний порт сервиса
    targetPort: 3000 # Порт, на который нужно перенаправлять трафик внутри пода&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Важные моменты в манифесте:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`resources:`: Puppeteer очень требователен к ресурсам. Указание запросов и лимитов помогает Kubernetes правильно размещать поды.&lt;/li&gt;
&lt;li&gt;`readiness/livenessProbe`: Пробы готовности и жизнеспособности позволяют Kubernetes понять, работает ли под корректно.&lt;/li&gt;
&lt;li&gt;`/dev/shm`: Chromium использует разделяемую память. Увеличение её размера через `emptyDir` предотвращает частые сбои.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Запуск и проверка&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# Переходим в корневую папку проекта
cd ..

# Применяем манифест
kubectl apply -f deployment.yaml

# Наблюдаем за созданием подов
kubectl get pods -l app=puppeteer-renderer -w

# Проверяем сервис. EXTERNAL-IP будет &amp;lt;pending&amp;gt;, это нормально.
kubectl get service pptr-renderer-service
# NAME                  TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
# pptr-renderer-service LoadBalancer   10.43.123.123   &amp;lt;pending&amp;gt;     80:31185/TCP   1m&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Шаг 4: Тестирование через Port-Forward&lt;/h3&gt;
&lt;p&gt;`kubectl port-forward` — это мощный инструмент для отладки, который создает безопасный тоннель с вашего компьютера напрямую к сервису в кластере.&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Откройте новый терминал&lt;/b&gt; и выполните команду:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;kubectl port-forward service/pptr-renderer-service 8080:80&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;`8080` — это порт на вашем локальном компьютере (`localhost`).&lt;/li&gt;
&lt;li&gt;`80` — это порт, который слушает наш `Service` в Kubernetes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Вы увидите сообщение `Forwarding from 127.0.0.1:8080 -&gt; 3000`. &lt;b&gt;Не закрывайте этот терминал.&lt;/b&gt;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;b&gt;Откройте третий терминал&lt;/b&gt; и отправьте тестовый запрос на `localhost:8080`:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;curl -o k8s-site.png &amp;quot;http://localhost:8080/render?url=https://kubernetes.io&amp;amp;width=1440&amp;amp;height=900&amp;amp;dsf=2&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Если все сделано правильно, в вашей текущей папке появится файл `k8s-site.png` со скриншотом сайта Kubernetes в высоком разрешении!&lt;/p&gt;
&lt;h3&gt;Итоги и рекомендации для продакшена&lt;/h3&gt;
&lt;p&gt;Мы успешно создали и развернули масштабируемый сервис для создания скриншотов. Мы убедились, что он работает локально в `k3s`, используя `LoadBalancer` и `port-forward` для удобной отладки.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Когда придет время переходить в продуктивное окружение (например, в облако Google/AWS):&lt;/b&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Тип Сервиса:&lt;/b&gt; Вам не нужно ничего менять! Просто примените тот же `deployment.yaml` с `type: LoadBalancer`. Облачный провайдер автоматически подхватит этот сервис и выделит ему настоящий публичный IP-адрес.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ingress:&lt;/b&gt; Для более гибкого управления трафиком (маршрутизация по хостам, SSL/TLS) используйте `Ingress Controller`. Он будет направлять внешний трафик на ваш сервис, который уже можно будет сделать типа `ClusterIP`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Автомасштабирование:&lt;/b&gt; Настройте `HorizontalPodAutoscaler` (HPA), чтобы Kubernetes автоматически добавлял или удалял поды в зависимости от нагрузки (например, по CPU).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Логирование и Мониторинг:&lt;/b&gt; Настройте централизованный сбор логов (EFK/Loki) и мониторинг метрик (Prometheus/Grafana), чтобы следить за здоровьем и производительностью сервиса.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Этот пример — отличная основа, которую можно развивать и адаптировать под самые разные задачи, от простых утилит до сложных систем автоматизации контента.&lt;/p&gt;
&lt;p&gt;Мне вот удалось rbc заскринить вот так, потом может что то сделаю для статеек с медиума.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;curl -o rbc111.png &amp;quot;http://localhost:8080/render?url=https://rbc.ru&amp;amp;width=1440&amp;amp;height=900&amp;amp;dsf=2&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Вот что вышло &lt;a href="http://a.gavrilov.info/data/posts/rbc111.png"&gt;png тут&lt;/a&gt;&lt;/p&gt;
</description>
</item>


</channel>
</rss>