diff --git a/Dockerfile b/Dockerfile index 3b39e307d..8bc3e443d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -175,15 +175,16 @@ RUN pip install --no-cache-dir --upgrade pip && \ RUN crawl4ai-setup -RUN playwright install --with-deps - -RUN mkdir -p /home/appuser/.cache/ms-playwright \ +RUN playwright install --with-deps \ + && 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 + && chown -R appuser:appuser /home/appuser/.cache/ms-playwright \ + && rm -rf /root/.cache/ms-playwright -RUN crawl4ai-doctor +RUN crawl4ai-doctor \ + && 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..60e6eb53a 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,27 @@ 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): + for command, cleanup in ( + ("playwright install --with-deps", "rm -rf /root/.cache/ms-playwright"), + ("crawl4ai-doctor", "-name '*.core' -delete"), + ): + layer = re.search( + rf"^RUN {re.escape(command)}(?P(?:(?!^[A-Z]+\s).)*)", + dockerfile, + re.MULTILINE | re.DOTALL, + ) + assert layer, f"Dockerfile must run {command}" + assert cleanup in layer.group( + "body" + ), f"{cleanup} must run in the {command} layer" + def test_runs_as_non_root(self, dockerfile): assert re.search(r"^USER\s+appuser", dockerfile, re.MULTILINE)