diff --git a/Dockerfile b/Dockerfile index 3b39e307d..9d64dbd0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -173,17 +173,16 @@ RUN pip install --no-cache-dir --upgrade pip && \ python -c "import crawl4ai; print('✅ crawl4ai is ready to rock!')" && \ python -c "from playwright.sync_api import sync_playwright; print('✅ Playwright is feeling dramatic!')" -RUN crawl4ai-setup - -RUN playwright install --with-deps - -RUN mkdir -p /home/appuser/.cache/ms-playwright \ +RUN crawl4ai-setup \ + && playwright install --with-deps chromium \ + && crawl4ai-doctor \ + && mkdir -p /home/appuser/.cache/ms-playwright \ && cp -r /root/.cache/ms-playwright/chromium-* \ /root/.cache/ms-playwright/chromium_headless_shell-* \ /home/appuser/.cache/ms-playwright/ \ - && chown -R appuser:appuser /home/appuser/.cache/ms-playwright - -RUN crawl4ai-doctor + && chown -R appuser:appuser /home/appuser/.cache/ms-playwright \ + && rm -rf /root/.cache/ms-playwright \ + && find "${APP_HOME}" -maxdepth 1 -type f -name '*.core' -delete # Ensure all cache directories belong to appuser # This fixes permission issues with .cache/url_seeder and other runtime cache dirs diff --git a/deploy/docker/tests/test_security_container_posture.py b/deploy/docker/tests/test_security_container_posture.py index 3399d6bd1..7c36c0b30 100644 --- a/deploy/docker/tests/test_security_container_posture.py +++ b/deploy/docker/tests/test_security_container_posture.py @@ -46,8 +46,9 @@ def test_no_redis_expose(self, dockerfile): stripped = line.strip() if stripped.startswith("#"): continue - assert not re.match(r"EXPOSE\s+.*\b6379\b", stripped), \ - "redis port 6379 must not be EXPOSEd" + assert not re.match( + r"EXPOSE\s+.*\b6379\b", stripped + ), "redis port 6379 must not be EXPOSEd" def test_app_dir_root_owned_readonly(self, dockerfile): assert "chown -R root:root ${APP_HOME}" in dockerfile @@ -66,10 +67,33 @@ def test_playwright_headless_shell_is_copied_to_runtime_cache(self, dockerfile): dockerfile, re.DOTALL, ) - assert cache_copy, "Dockerfile must copy Playwright artifacts into appuser's cache" + assert ( + cache_copy + ), "Dockerfile must copy Playwright artifacts into appuser's cache" assert "chromium-*" in cache_copy.group("artifacts") assert "chromium_headless_shell-*" in cache_copy.group("artifacts") + def test_build_artifacts_removed_before_layer_commit(self, dockerfile): + layer = re.search( + r"^RUN crawl4ai-setup(?P(?:(?!^[A-Z]+\s).)*)", + dockerfile, + re.MULTILINE | re.DOTALL, + ) + assert layer, "Dockerfile must run crawl4ai-setup" + positions = [] + for command in ( + "playwright install --with-deps chromium", + "crawl4ai-doctor", + "cp -r /root/.cache/ms-playwright/chromium-*", + "rm -rf /root/.cache/ms-playwright", + "-name '*.core' -delete", + ): + assert command in layer.group( + "body" + ), f"{command} must run in the crawl4ai-setup layer" + positions.append(layer.group("body").index(command)) + assert positions == sorted(positions), "Docker build steps must remain ordered" + def test_runs_as_non_root(self, dockerfile): assert re.search(r"^USER\s+appuser", dockerfile, re.MULTILINE)