diff --git a/.forgejo/workflows/docker-build-push.yaml b/.forgejo/workflows/docker-build-push.yaml deleted file mode 100644 index 340cceb..0000000 --- a/.forgejo/workflows/docker-build-push.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build and Push Docker Image - -on: - push: - branches: - - main - paths: - - '**/*' # Триггер при любом изменении репозитория - -jobs: - build-and-push: - runs-on: docker - - container: - image: docker:24.0.1 - - steps: - # Установка Docker CLI (если не в базовом образе) - - name: Setup Docker CLI - run: | - apk add --no-cache docker-cli - - # Авторизация в Docker Hub - токен необходимо добавить в Secrets - - name: Login to Docker Hub - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - # Клонирование исходников — встроено в Forgejo Actions - - # Сборка Docker образа - - name: Build Docker Image - run: | - docker build -t ${DOCKER_USERNAME}/playwright-domain-scanner:latest . - - # Push образ на Docker Hub - - name: Push Docker Image - run: | - docker push ${DOCKER_USERNAME}/playwright-domain-scanner:latest - diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/docker-build-push.yaml new file mode 100644 index 0000000..be6ac82 --- /dev/null +++ b/.github/workflows/docker-build-push.yaml @@ -0,0 +1,28 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + +jobs: + build-and-push: + runs-on: ubuntu-latest + environment: dockerhub + + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: ${{ secrets.DOCKER_USERNAME }}/gekata:latest + diff --git a/Dockerfile b/Dockerfile index f95e58b..adf380a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,48 @@ -# Use official Node.js LTS base image -FROM node:20-slim +# Base minimal Debian +FROM debian:bookworm-slim -# Install dependencies for running Chromium -RUN apt-get update && apt-get install -y \ - ca-certificates \ - fonts-liberation \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libcups2 \ - libdbus-1-3 \ - libdrm2 \ - libgbm1 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libx11-xcb1 \ - libxcomposite1 \ - libxdamage1 \ - libxrandr2 \ - xdg-utils \ - wget \ - --no-install-recommends && \ - rm -rf /var/lib/apt/lists/* +# Prevent tzdata prompts +ENV DEBIAN_FRONTEND=noninteractive -# Set working directory -WORKDIR /usr/src/app +# Install Node.js, Chromium and minimal runtime libs +# Note: chromium package on Debian provides /usr/bin/chromium +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg \ + nodejs npm \ + chromium \ + # Minimal GUI/Chromium runtime libs often needed by Playwright Chromium + libx11-6 libxcomposite1 libxdamage1 libxrandr2 libxkbcommon0 \ + libgtk-3-0 libnss3 libdrm2 libgbm1 libasound2 fonts-liberation \ + # Useful for font rendering + fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* -# Copy package files and install dependencies +# App directory +WORKDIR /app + +# Install only production deps COPY package*.json ./ -COPY ignore-domains.txt ./ -RUN npm ci +ENV CI=true +RUN npm ci --omit=dev -# Install Playwright browsers (Chromium) -RUN npx playwright install chromium - -# Copy app sources +# Copy source COPY . . -# Expose port +# Security: run as non-root +RUN useradd -ms /bin/bash nodeuser && chown -R nodeuser:nodeuser /app +USER nodeuser + +# Environment for service +ENV PORT=3000 \ + # Ensure Playwright uses system Chromium and does not download browsers + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ + PLAYWRIGHT_BROWSERS_PATH=0 \ + # Explicit executable if needed in code; here server uses default, so optional + CHROMIUM_PATH=/usr/bin/chromium + +# Expose service port EXPOSE 3000 -# Run the service -CMD ["npm", "start"] +# Start the service +CMD ["node", "server.js"] diff --git a/server.js b/server.js index f6ef7fe..dde4a67 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,22 @@ +// server.js const express = require('express'); const { chromium } = require('playwright'); const app = express(); const port = process.env.PORT || 3000; +// Использовать системный Chromium, если задан путь (например, /usr/bin/chromium в Debian) +const executablePath = process.env.CHROMIUM_PATH || undefined; // можно оставить undefined, если Chromium в PATH [1][2] + +// Базовый набор флагов для контейнера без systemd/dbus и без install-deps +const chromiumArgs = [ + '--no-sandbox', // запуск без setuid sandbox в контейнере [14] + '--disable-setuid-sandbox', // отключение setuid sandbox [14] + '--disable-dev-shm-usage', // использовать /tmp вместо /dev/shm (если нет --ipc=host) [15][16] + '--disable-gpu', // headless окружение [14] + '--no-zygote', // упрощение процессов в контейнере [14] +]; + app.use(express.json()); function extractDomain(url) { @@ -23,12 +36,17 @@ app.get('/domains', async (req, res) => { const url = `https://${domain}`; const seenDomains = new Set(); + let browser; + let context; try { - const browser = await chromium.launch({ - args: ['--no-sandbox', '--disable-setuid-sandbox'] + browser = await chromium.launch({ + executablePath, // берётся из CHROMIUM_PATH при наличии [1][2] + headless: true, // явный headless режим для контейнера [14] + args: chromiumArgs, // флаги для стабильности в Docker [15][14] }); - const context = await browser.newContext(); + + context = await browser.newContext(); const page = await context.newPage(); page.on('request', request => { @@ -37,15 +55,20 @@ app.get('/domains', async (req, res) => { }); await page.goto(url, { waitUntil: 'load', timeout: 30000 }); + + // Фильтрация доменов после закрытия страницы + await context.close(); await browser.close(); - // Фильтрация доменов - const filteredDomains = Array.from(seenDomains).filter(d => - !d.includes('doubleclick') && !d.includes('google') - ).sort(); + const filteredDomains = Array.from(seenDomains) + .filter(d => !d.includes('doubleclick') && !d.includes('google')) + .sort(); res.json({ domains: filteredDomains }); } catch (e) { + // Безопасно закрыть ресурсы при ошибке + try { if (context) await context.close(); } catch {} + try { if (browser) await browser.close(); } catch {} res.status(500).json({ error: e.message || 'Internal server error' }); } });