{
    "version": "https:\/\/jsonfeed.org\/version\/1.1",
    "title": "Yuriy Gavrilov: posts tagged k3s",
    "_rss_description": "Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov",
    "_rss_language": "en",
    "_itunes_email": "yvgavrilov@gmail.com",
    "_itunes_categories_xml": "",
    "_itunes_image": "https:\/\/gavrilov.info\/pictures\/userpic\/userpic-square@2x.jpg?1643451008",
    "_itunes_explicit": "no",
    "home_page_url": "https:\/\/gavrilov.info\/tags\/k3s\/",
    "feed_url": "https:\/\/gavrilov.info\/tags\/k3s\/json\/",
    "icon": "https:\/\/gavrilov.info\/pictures\/userpic\/userpic@2x.jpg?1643451008",
    "authors": [
        {
            "name": "Yuriy Gavrilov - B[u]g - for charity.gavrilov.eth",
            "url": "https:\/\/gavrilov.info\/",
            "avatar": "https:\/\/gavrilov.info\/pictures\/userpic\/userpic@2x.jpg?1643451008"
        }
    ],
    "items": [
        {
            "id": "299",
            "url": "https:\/\/gavrilov.info\/all\/sravnitelny-analiz-self-hosted-s3-sovmestimyh-hranilisch\/",
            "title": "Сравнительный анализ self-hosted S3-совместимых хранилищ",
            "content_html": "<p>Четкое сравнение семи self-hosted S3-совместимых решений для хранения данных.<\/p>\n<p>Оригинал тут: <a href=\"https:\/\/www.repoflow.io\/blog\/benchmarking-self-hosted-s3-compatible-storage-a-practical-performance-comparison\">Команда RepoFlow. 9 августа 2025 г.<\/a><\/p>\n<p>Локальное (self-hosted) объектное хранилище — это отличный выбор для разработчиков и команд, которые хотят иметь полный контроль над хранением и доступом к своим данным. Независимо от того, заменяете ли вы Amazon S3, размещаете внутренние файлы, создаете CI-конвейер или обслуживаете репозитории пакетов, уровень хранения может значительно повлиять на скорость и стабильность.<\/p>\n<p>Мы протестировали семь популярных решений для объектного хранения, поддерживающих протокол S3. Цель состояла в том, чтобы сравнить их производительность в идентичных условиях, используя реальные операции загрузки и скачивания.<\/p>\n<h3>Тестируемые решения<\/h3>\n<p>Каждое из следующих решений было развернуто с помощью Docker на одном и том же сервере без монтирования томов и без специальной настройки:<\/p>\n<ol start=\"1\">\n<li>`MinIO`<\/li>\n<li>`Ceph`<\/li>\n<li>`SeaweedFS`<\/li>\n<li>`Garage`<\/li>\n<li>`Zenko` (Scality Cloudserver)<\/li>\n<li>`LocalStack`<\/li>\n<li>`RustFS`<\/li>\n<\/ol>\n<h3>Скорость последовательного скачивания<\/h3>\n<p>Средняя скорость скачивания одного файла разного размера.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.42.26.png\" width=\"1270\" height=\"656\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости последовательного скачивания для малых файлов размером 50 КБ и 200 КБ. По оси Y — скорость в МБ\/с, по оси X — размер файла. Сравниваются Garage, Localstack, Minio, Zenko, Ceph, RustFS, SeaweedFS.]<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.42.57.png\" width=\"1264\" height=\"650\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости последовательного скачивания для больших файлов размером 10 МБ, 50 МБ, 100 МБ и 1 ГБ. По оси Y — скорость в МБ\/с, по оси X — размер файла. Сравниваются те же решения.]<\/p>\n<h3>Скорость последовательной загрузки<\/h3>\n<p>Средняя скорость загрузки одного файла разного размера.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.43.18.png\" width=\"1278\" height=\"648\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости последовательной загрузки для малых файлов размером 50 КБ и 200 КБ. По оси Y — скорость в МБ\/с, по оси X — размер файла. Сравниваются те же решения.]<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.43.34.png\" width=\"1252\" height=\"636\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости последовательной загрузки для больших файлов размером 10 МБ, 50 МБ, 100 МБ и 1 ГБ. По оси Y — скорость в МБ\/с, по оси X — размер файла. Сравниваются те же решения.]<\/p>\n<h3>Производительность листинга<\/h3>\n<p>Измеряет время, необходимое для получения списка всех 2000 тестовых объектов в бакете с использованием разных размеров страницы (100, 500 и 1000 результатов на запрос).<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.44.02.png\" width=\"1280\" height=\"648\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График производительности листинга. По оси Y — время в мс, по оси X — количество результатов на страницу (100, 500, 1000). Сравниваются те же решения.]<\/p>\n<h3>Скорость параллельной загрузки<\/h3>\n<p>Измеряет время, необходимое для параллельной загрузки нескольких файлов одинакового размера. Скорость загрузки рассчитывается по формуле:<\/p>\n<p>(number of files × file size) ÷ total time<\/p>\n<h4>Скорость параллельной загрузки – файлы 1 МБ<\/h4>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.44.22.png\" width=\"1304\" height=\"748\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости параллельной загрузки файлов размером 1 МБ. По оси Y — скорость в МБ\/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]<\/p>\n<h4>Скорость параллельной загрузки – файлы 10 МБ<\/h4>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.45.06.png\" width=\"1254\" height=\"650\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости параллельной загрузки файлов размером 10 МБ. По оси Y — скорость в МБ\/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]<\/p>\n<h4>Скорость параллельной загрузки – файлы 100 МБ<\/h4>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.45.23.png\" width=\"1278\" height=\"652\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости параллельной загрузки файлов размером 100 МБ. По оси Y — скорость в МБ\/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]<\/p>\n<h3>Скорость параллельного скачивания<\/h3>\n<p>Измеряет время, необходимое для параллельного скачивания нескольких файлов одинакового размера. Скорость скачивания рассчитывается по формуле:<\/p>\n<p>(number of files × file size) ÷ total time<\/p>\n<h4>Скорость параллельного скачивания – файлы 1 МБ<\/h4>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.45.45.png\" width=\"1278\" height=\"650\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости параллельного скачивания файлов размером 1 МБ. По оси Y — скорость в МБ\/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]<\/p>\n<h4>Скорость параллельного скачивания – файлы 10 МБ<\/h4>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.46.02.png\" width=\"1280\" height=\"644\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости параллельного скачивания файлов размером 10 МБ. По оси Y — скорость в МБ\/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]<\/p>\n<h4>Скорость параллельного скачивания – файлы 100 МБ<\/h4>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.46.17.png\" width=\"1284\" height=\"670\" alt=\"\" \/>\n<\/div>\n<p>[Изображение: График скорости параллельного скачивания файлов размером 100 МБ. По оси Y — скорость в МБ\/с, по оси X — количество параллельных потоков (5, 10, 20). Сравниваются те же решения.]<\/p>\n<h3>Как проводились тесты<\/h3>\n<p>Для каждого решения мы:<\/p>\n<ol start=\"1\">\n<li>Загружали и скачивали файлы 7 различных размеров: 50 КБ, 200 КБ, 1 МБ, 10 МБ, 50 МБ, 100 МБ и 1 ГБ.<\/li>\n<li>Повторяли каждую загрузку и скачивание 20 раз для получения стабильных средних значений.<\/li>\n<li>Измеряли среднюю скорость загрузки и скачивания в мегабайтах в секунду (МБ\/с).<\/li>\n<li>Выполняли все тесты на одной и той же машине, используя стандартный Docker-контейнер для каждой системы хранения, без внешних томов, монтирования или кешей.<\/li>\n<\/ol>\n<p>Все решения тестировались в одноузловой конфигурации для обеспечения согласованности. Хотя некоторые системы (например, `Ceph`) спроектированы для лучшей производительности в кластерной среде, мы использовали одинаковые условия для всех решений, чтобы гарантировать справедливое сравнение.<\/p>\n<h3>Заключительные мысли<\/h3>\n<p>Эти результаты показывают, как каждое решение вело себя в нашей конкретной тестовой среде с одним узлом. Их следует рассматривать как относительное сравнение соотношений производительности, а не как абсолютные жесткие значения, которые будут применимы в любой конфигурации.<\/p>\n<p>При выборе подходящего решения для хранения данных учитывайте типичные размеры файлов, которые вы будете хранить, поскольку одни системы лучше справляются с маленькими файлами, а другие преуспевают с большими. Также подумайте об основных возможностях, которые вам требуются, таких как масштабируемость, репликация, долговечность или встроенный графический интерфейс. Наконец, помните, что производительность может сильно отличаться между одноузловыми и многоузловыми кластерами.<\/p>\n<p>Наши тесты предоставляют базовый уровень для понимания того, как эти системы соотносятся в идентичных условиях, но ваша реальная производительность будет зависеть от вашего конкретного оборудования, рабочей нагрузки и конфигурации.<\/p>\n",
            "date_published": "2025-12-08T00:50:17+03:00",
            "date_modified": "2025-12-08T00:50:03+03:00",
            "tags": [
                "big data",
                "Data Engineer",
                "Dev",
                "k3s",
                "Programming",
                "s3"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.42.26.png",
            "_date_published_rfc2822": "Mon, 08 Dec 2025 00:50:17 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "299",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.42.26.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.42.57.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.43.18.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.43.34.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.44.02.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.44.22.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.45.06.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.45.23.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.45.45.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.46.02.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-00.46.17.png"
                ]
            }
        },
        {
            "id": "297",
            "url": "https:\/\/gavrilov.info\/all\/sozdanie-masshtabiruemogo-servisa-dlya-skrinshotov-saytov-na-nod\/",
            "title": "Создание масштабируемого сервиса для скриншотов сайтов на Node.js, Puppeteer и Kubernetes",
            "content_html": "<p>В современном вебе часто возникает задача автоматического создания скриншотов веб-страниц. Это может быть нужно для генерации превью ссылок, мониторинга доступности сайтов, создания PDF-отчетов или даже для работы сервисов по автоматизации контента.<\/p>\n<p>В этой статье мы пошагово создадим свой собственный, надежный и масштабируемый микросервис для рендеринга скриншотов. Мы будем использовать мощную связку технологий:<\/p>\n<ul>\n<li><b>Node.js & Express:<\/b> для создания легковесного API.<\/li>\n<li><b>Puppeteer:<\/b> для управления headless-браузером Chromium и создания скриншотов.<\/li>\n<li><b>Docker:<\/b> для упаковки нашего приложения в изолированный контейнер.<\/li>\n<li><b>Kubernetes (k3s):<\/b> для оркестрации, масштабирования и обеспечения отказоустойчивости нашего сервиса.<\/li>\n<\/ul>\n<p>В итоге мы получим простое API, которое по GET-запросу с URL-адресом будет возвращать готовую картинку.<\/p>\n<h3>Зачем нужен собственный сервис?<\/h3>\n<p>Хотя существуют готовые SaaS-решения вроде <a href=\"https:\/\/urlbox.com,\">https:\/\/urlbox.com,<\/a> создание собственного сервиса дает несколько ключевых преимуществ:<\/p>\n<ol start=\"1\">\n<li><b>Контроль:<\/b> Вы полностью контролируете окружение, версии браузера и параметры рендеринга.<\/li>\n<li><b>Безопасность:<\/b> Вы можете запускать сервис в своей приватной сети для обработки внутренних ресурсов, недоступных извне.<\/li>\n<li><b>Стоимость:<\/b> При большом объеме запросов собственный сервис может оказаться значительно дешевле SaaS.<\/li>\n<li><b>Гибкость:<\/b> Вы можете легко расширить функциональность, добавив, например, генерацию PDF, выполнение кастомных скриптов на странице и многое другое.<\/li>\n<\/ol>\n<h3>Шаг 1: Создание Node.js приложения<\/h3>\n<p>Сначала создадим основу нашего сервиса — Express-приложение, которое будет принимать запросы и управлять Puppeteer.<\/p>\n<h4>Структура проекта<\/h4>\n<p>Создадим папку для нашего проекта и внутри нее — необходимые файлы.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">puppeteer-k8s\/\n├── dockerb\/\n│   ├── Dockerfile\n│   ├── .dockerignore\n│   ├── server.js\n│   ├── package.json\n│   └── package-lock.json\n└── deployment.yaml<\/code><\/pre><p>*Обратите внимание: мы поместили все, что относится к Docker, в подпапку `dockerb` для удобства.*<\/p>\n<h4>Код сервера (`server.js`)<\/h4>\n<p>Это ядро нашего приложения. Оно принимает HTTP-запросы, запускает Puppeteer, переходит на указанный URL и делает скриншот. Мы также добавим полезные параметры для управления качеством и размером.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">\/\/ server.js. -- в моем докере server_v3.js\n\nconst express = require('express');\nconst puppeteer = require('puppeteer');\n\nconst app = express();\nconst PORT = 3000;\n\napp.get('\/render', async (req, res) =&gt; {\n    \/\/ Добавляем новые параметры: quality и dsf (deviceScaleFactor)\n    const { \n        url, \n        width, \n        height, \n        format = 'png', \n        fullPage = 'false',\n        quality, \/\/ Качество для jpeg\/webp (0-100)\n        dsf = '1' \/\/ Device Scale Factor. Для Retina-качества используйте '2'\n    } = req.query;\n\n    if (!url) {\n        return res.status(400).json({\n            error: 'Параметр &quot;url&quot; обязателен.',\n            example: '\/render?url=https:\/\/example.com'\n        });\n    }\n\n    let browser;\n    try {\n        console.log(`[${new Date().toISOString()}] Начинаем рендеринг для: ${url}`);\n        \n        \/\/ Добавляем аргументы для улучшения рендеринга\n        browser = await puppeteer.launch({\n            args: [\n                '--no-sandbox',\n                '--disable-setuid-sandbox',\n                '--font-render-hinting=none', \/\/ Может улучшить отрисовку шрифтов\n                '--disable-infobars'       \/\/ Убирает инфо-панели\n            ]\n        });\n        \n        const page = await browser.newPage();\n\n        const viewportWidth = parseInt(width, 10) || 1280;\n        const viewportHeight = parseInt(height, 10) || 720;\n        const deviceScaleFactor = parseInt(dsf, 10) || 1;\n\n        \/\/ Устанавливаем viewport с учетом deviceScaleFactor\n        await page.setViewport({\n            width: viewportWidth,\n            height: viewportHeight,\n            deviceScaleFactor: deviceScaleFactor\n        });\n\n        console.log(`Viewport: ${viewportWidth}x${viewportHeight}, DeviceScaleFactor: ${deviceScaleFactor}`);\n\n        await page.goto(url, { waitUntil: 'networkidle2' });\n\n        \/\/ Если есть &quot;ленивая&quot; загрузка изображений, можно &quot;проскроллить&quot; страницу\n        \/\/ Это необязательный шаг, но он помогает прогрузить все картинки\n        if (fullPage === 'true') {\n             await page.evaluate(async () =&gt; {\n                await new Promise((resolve) =&gt; {\n                    let totalHeight = 0;\n                    const distance = 100;\n                    const timer = setInterval(() =&gt; {\n                        const scrollHeight = document.body.scrollHeight;\n                        window.scrollBy(0, distance);\n                        totalHeight += distance;\n\n                        if (totalHeight &gt;= scrollHeight) {\n                            clearInterval(timer);\n                            resolve();\n                        }\n                    }, 100);\n                });\n            });\n        }\n        \n        const screenshotOptions = {\n            type: (format === 'jpeg' || format === 'webp') ? format : 'png',\n            fullPage: fullPage === 'true'\n        };\n\n        \/\/ Устанавливаем качество, если параметр был передан\n        if ((screenshotOptions.type === 'jpeg' || screenshotOptions.type === 'webp') &amp;&amp; quality) {\n            screenshotOptions.quality = parseInt(quality, 10);\n        } else if (screenshotOptions.type === 'jpeg') {\n            screenshotOptions.quality = 90; \/\/ Ставим хорошее качество по умолчанию для JPEG\n        }\n\n        const imageBuffer = await page.screenshot(screenshotOptions);\n        \n        console.log(`[${new Date().toISOString()}] Рендеринг успешно завершен.`);\n\n        res.setHeader('Content-Type', `image\/${screenshotOptions.type}`);\n        res.send(imageBuffer);\n\n    } catch (error) {\n        console.error(`[${new Date().toISOString()}] Ошибка рендеринга для ${url}:`, error.message);\n        res.status(500).json({ \n            error: 'Не удалось отрендерить страницу.',\n            details: error.message \n        });\n    } finally {\n        if (browser) {\n            await browser.close();\n        }\n    }\n});\n\n\napp.get('\/', (req, res) =&gt; {\n    res.setHeader('Content-Type', 'text\/plain; charset=utf-8');\n    res.send(`\n    API для рендеринга сайтов (v2 - Улучшенное качество)\n    ----------------------------------------------------\n    Используйте GET-запрос на \/render с параметрами.\n\n    Обязательные параметры:\n      - url: Адрес сайта для скриншота.\n\n    Необязательные параметры:\n      - width: Ширина окна (по умолч. 1280).\n      - height: Высота окна (по умолч. 720).\n      - format: Формат файла ('png', 'jpeg', 'webp'). По умолч. 'png'.\n      - fullPage: Сделать скриншот всей страницы ('true' или 'false'). По умолч. 'false'.\n      - dsf: Device Scale Factor для Retina\/HiDPI качества (например, '2'). По умолч. '1'.\n      - quality: Качество для jpeg\/webp от 0 до 100.\n\n    Пример для высокого качества (Retina):\n    curl -o site_retina.png &quot;http:\/\/localhost:3000\/render?url=https:\/\/github.com&amp;dsf=2&amp;width=1440&amp;height=900&quot;\n    \n    Пример для JPEG с максимальным качеством:\n    curl -o site_hq.jpeg &quot;http:\/\/localhost:3000\/render?url=https:\/\/apple.com&amp;format=jpeg&amp;quality=100&amp;width=1920&amp;height=1080&quot;\n    `);\n});\n\napp.listen(PORT, () =&gt; {\n    console.log(`Сервис рендеринга запущен на http:\/\/localhost:${PORT}`);\n});<\/code><\/pre><p>*Запустите `npm install` в папке `dockerb`, чтобы сгенерировать `package-lock.json`.*<\/p>\n<h3>Шаг 2: Создание Docker-образа<\/h3>\n<p>Теперь упакуем наше приложение. Этот шаг — самый каверзный, так как Puppeteer требует особого подхода к правам доступа и зависимостям.<\/p>\n<p><a href=\"https:\/\/hub.docker.com\/repository\/docker\/1325gy\/my_dev\/tags\/v4\">мой пример тут образа <\/a><\/p>\n<h4>Файл `.dockerignore`<\/h4>\n<p>Крайне важно не копировать локальную папку `node_modules` в образ. Они должны устанавливаться внутри контейнера для правильной архитектуры.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># .dockerignore\nnode_modules<\/code><\/pre><h4>Финальный `Dockerfile`<\/h4>\n<p>Этот `Dockerfile` — результат множества проб и ошибок. Он решает все основные проблемы:<\/p>\n<ul>\n<li>Устанавливает необходимые системные библиотеки для Chromium.<\/li>\n<li>Создает непривилегированного пользователя `pptruser` с домашней директорией.<\/li>\n<li>Устанавливает `npm`-зависимости и браузер от имени этого пользователя, чтобы избежать проблем с правами доступа.<\/li>\n<\/ul>\n<pre class=\"e2-text-code\"><code class=\"\"># 1. Базовый образ\nFROM node:18-slim\n\n# 2. Установка системных зависимостей\nRUN apt-get update &amp;&amp; apt-get install -y \\\n    ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \\\n    libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \\\n    libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 \\\n    libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \\\n    libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 \\\n    libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils \\\n    &amp;&amp; rm -rf \/var\/lib\/apt\/lists\/*\n\n# 3. создаем пользователя с домом\n# Флаг -m (--create-home) гарантирует создание папки \/home\/pptruser\nRUN useradd -r -m -g root -s \/bin\/bash pptruser\n\n# 4. Устанавливаем рабочую директорию\nWORKDIR \/app\n\n# 5. Копируем файлы зависимостей и сразу назначаем владельцем pptruser\n# (современный и более чистый способ, чем chown после)\nCOPY --chown=pptruser:root package*.json .\/\n\n# 6. Переключаемся на нашего пользователя\nUSER pptruser\n\n# 7. Устанавливаем всё от имени pptruser. Кэш ляжет в \/home\/pptruser\/.cache\nRUN npm ci\nRUN npx puppeteer browsers install chrome\n\n# 8. Копируем остальной код, также сразу назначая владельца\nCOPY --chown=pptruser:root . .\n\n# 9. Открываем порт и запускаем\nEXPOSE 3000\nCMD [ &quot;node&quot;, &quot;server.js&quot; ]<\/code><\/pre><h4>Сборка и отправка образа<\/h4>\n<p>Замените `your_docker_id` на ваш логин в Docker Hub.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># Переходим в папку с Dockerfile\ncd puppeteer-k8s\/dockerb\n\n# Собираем образ\ndocker build -t your_docker_id\/puppeteer-renderer:v1 .\n\n# Отправляем в репозиторий\ndocker push your_docker_id\/puppeteer-renderer:v1<\/code><\/pre><h3>Шаг 3: Развертывание в Kubernetes<\/h3>\n<p>Мы будем использовать `k3s` — легковесный дистрибутив Kubernetes.  Можно еще тут посмотреть пример <a href=\"https:\/\/github.com\/jamesheronwalker\/urlbox-puppeteer-kubernetes-demo\">https:\/\/github.com\/jamesheronwalker\/urlbox-puppeteer-kubernetes-demo<\/a><\/p>\n<p>или вот статейка еще <a href=\"https:\/\/urlbox.com\/guides\/kubernetes-website-screenshots\">https:\/\/urlbox.com\/guides\/kubernetes-website-screenshots<\/a><\/p>\n<h4>Установка окружения (Local)<\/h4>\n<pre class=\"e2-text-code\"><code class=\"\"># Установка lima и k3s (если еще не сделано)\nbrew install lima\nlimactl start template:\/\/k3s\n\n# Настройка kubectl для работы с кластером\nexport KUBECONFIG=$(limactl list k3s --format 'unix:\/\/{{.Dir}}\/copied-from-guest\/kubeconfig.yaml')\n\n# Проверка, что все работает\nkubectl get nodes<\/code><\/pre><h4>Манифест развертывания (`deployment.yaml`)<\/h4>\n<p>Этот YAML-файл описывает желаемое состояние нашего приложения в кластере. Мы будем использовать тип сервиса `LoadBalancer` — он идеально подходит для будущего развертывания в облаке, а для локальной разработки мы будем использовать `port-forward`.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># deployment.yaml\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: puppeteer-renderer\nspec:\n  replicas: 2 # Запускаем два пода для отказоустойчивости\n  selector:\n    matchLabels:\n      app: puppeteer-renderer\n  template:\n    metadata:\n      labels:\n        app: puppeteer-renderer\n    spec:\n      containers:\n      - name: renderer\n        image: your_docker_id\/puppeteer-renderer:v1 # &lt;&lt;&lt; ВАШ ОБРАЗ (мой тут docker pull 1325gy\/my_dev:v4)\n        ports:\n        - containerPort: 3000\n        resources:\n          requests:\n            cpu: &quot;500m&quot;\n            memory: &quot;1Gi&quot;\n          limits:\n            cpu: &quot;1&quot;\n            memory: &quot;2Gi&quot;\n        readinessProbe:\n          httpGet:\n            path: \/\n            port: 3000\n          initialDelaySeconds: 15\n          periodSeconds: 10\n        livenessProbe:\n          httpGet:\n            path: \/\n            port: 3000\n          initialDelaySeconds: 30\n          periodSeconds: 20\n        volumeMounts:\n        - name: dshm\n          mountPath: \/dev\/shm\n      volumes:\n      - name: dshm\n        emptyDir:\n          medium: Memory\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: pptr-renderer-service\nspec:\n  # Тип LoadBalancer - стандарт для облаков. Локально он останется в &lt;pending&gt;,\n  # но это не помешает нам использовать port-forward.\n  type: LoadBalancer \n  selector:\n    app: puppeteer-renderer\n  ports:\n  - protocol: TCP\n    port: 80 # Внешний порт сервиса\n    targetPort: 3000 # Порт, на который нужно перенаправлять трафик внутри пода<\/code><\/pre><p><b>Важные моменты в манифесте:<\/b><\/p>\n<ul>\n<li>`resources:`: Puppeteer очень требователен к ресурсам. Указание запросов и лимитов помогает Kubernetes правильно размещать поды.<\/li>\n<li>`readiness\/livenessProbe`: Пробы готовности и жизнеспособности позволяют Kubernetes понять, работает ли под корректно.<\/li>\n<li>`\/dev\/shm`: Chromium использует разделяемую память. Увеличение её размера через `emptyDir` предотвращает частые сбои.<\/li>\n<\/ul>\n<h4>Запуск и проверка<\/h4>\n<pre class=\"e2-text-code\"><code class=\"\"># Переходим в корневую папку проекта\ncd ..\n\n# Применяем манифест\nkubectl apply -f deployment.yaml\n\n# Наблюдаем за созданием подов\nkubectl get pods -l app=puppeteer-renderer -w\n\n# Проверяем сервис. EXTERNAL-IP будет &lt;pending&gt;, это нормально.\nkubectl get service pptr-renderer-service\n# NAME                  TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE\n# pptr-renderer-service LoadBalancer   10.43.123.123   &lt;pending&gt;     80:31185\/TCP   1m<\/code><\/pre><h3>Шаг 4: Тестирование через Port-Forward<\/h3>\n<p>`kubectl port-forward` — это мощный инструмент для отладки, который создает безопасный тоннель с вашего компьютера напрямую к сервису в кластере.<\/p>\n<ol start=\"1\">\n<li><b>Откройте новый терминал<\/b> и выполните команду:<\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">kubectl port-forward service\/pptr-renderer-service 8080:80<\/code><\/pre><ul>\n<li>`8080` — это порт на вашем локальном компьютере (`localhost`).<\/li>\n<li>`80` — это порт, который слушает наш `Service` в Kubernetes.<\/li>\n<\/ul>\n<p>Вы увидите сообщение `Forwarding from 127.0.0.1:8080 -> 3000`. <b>Не закрывайте этот терминал.<\/b><\/p>\n<ol start=\"2\">\n<li><b>Откройте третий терминал<\/b> и отправьте тестовый запрос на `localhost:8080`:<\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">curl -o k8s-site.png &quot;http:\/\/localhost:8080\/render?url=https:\/\/kubernetes.io&amp;width=1440&amp;height=900&amp;dsf=2&quot;<\/code><\/pre><p>Если все сделано правильно, в вашей текущей папке появится файл `k8s-site.png` со скриншотом сайта Kubernetes в высоком разрешении!<\/p>\n<h3>Итоги и рекомендации для продакшена<\/h3>\n<p>Мы успешно создали и развернули масштабируемый сервис для создания скриншотов. Мы убедились, что он работает локально в `k3s`, используя `LoadBalancer` и `port-forward` для удобной отладки.<\/p>\n<p><b>Когда придет время переходить в продуктивное окружение (например, в облако Google\/AWS):<\/b><\/p>\n<ol start=\"1\">\n<li><b>Тип Сервиса:<\/b> Вам не нужно ничего менять! Просто примените тот же `deployment.yaml` с `type: LoadBalancer`. Облачный провайдер автоматически подхватит этот сервис и выделит ему настоящий публичный IP-адрес.<\/li>\n<li><b>Ingress:<\/b> Для более гибкого управления трафиком (маршрутизация по хостам, SSL\/TLS) используйте `Ingress Controller`. Он будет направлять внешний трафик на ваш сервис, который уже можно будет сделать типа `ClusterIP`.<\/li>\n<li><b>Автомасштабирование:<\/b> Настройте `HorizontalPodAutoscaler` (HPA), чтобы Kubernetes автоматически добавлял или удалял поды в зависимости от нагрузки (например, по CPU).<\/li>\n<li><b>Логирование и Мониторинг:<\/b> Настройте централизованный сбор логов (EFK\/Loki) и мониторинг метрик (Prometheus\/Grafana), чтобы следить за здоровьем и производительностью сервиса.<\/li>\n<\/ol>\n<p>Этот пример — отличная основа, которую можно развивать и адаптировать под самые разные задачи, от простых утилит до сложных систем автоматизации контента.<\/p>\n<p>Мне вот удалось rbc заскринить вот так, потом может что то сделаю для статеек с медиума.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">curl -o rbc111.png &quot;http:\/\/localhost:8080\/render?url=https:\/\/rbc.ru&amp;width=1440&amp;height=900&amp;dsf=2&quot;<\/code><\/pre><p>Вот что вышло <a href=\"http:\/\/a.gavrilov.info\/data\/posts\/rbc111.png\">png тут<\/a><\/p>\n",
            "date_published": "2025-12-01T00:28:00+03:00",
            "date_modified": "2025-12-01T08:55:44+03:00",
            "tags": [
                "k3s",
                "Programming"
            ],
            "_date_published_rfc2822": "Mon, 01 Dec 2025 00:28:00 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "297",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": []
            }
        }
    ],
    "_e2_version": 4171,
    "_e2_ua_string": "Aegea 11.4 (v4171e)"
}