From 29bae06166a9b17838c68c8a5c34db5a5855eef4 Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Wed, 22 Jul 2026 23:32:27 +0800 Subject: [PATCH 1/2] fix(docker): remove build artifacts from image layers Signed-off-by: nightcityblade --- Dockerfile | 11 +++++---- .../tests/test_security_container_posture.py | 24 ++++++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) 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) From 14291fac78d88674f36625f70890885a841bff6b Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Wed, 29 Jul 2026 23:08:54 +0800 Subject: [PATCH 2/2] fix(docker): keep browser setup in one layer --- Dockerfile | 10 +++---- .../tests/test_security_container_posture.py | 28 +++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8bc3e443d..9d64dbd0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -173,17 +173,15 @@ 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 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 \ - && rm -rf /root/.cache/ms-playwright - -RUN crawl4ai-doctor \ + && rm -rf /root/.cache/ms-playwright \ && find "${APP_HOME}" -maxdepth 1 -type f -name '*.core' -delete # Ensure all cache directories belong to appuser diff --git a/deploy/docker/tests/test_security_container_posture.py b/deploy/docker/tests/test_security_container_posture.py index 60e6eb53a..7c36c0b30 100644 --- a/deploy/docker/tests/test_security_container_posture.py +++ b/deploy/docker/tests/test_security_container_posture.py @@ -74,19 +74,25 @@ def test_playwright_headless_shell_is_copied_to_runtime_cache(self, dockerfile): 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( + 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", ): - 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( + assert command in layer.group( "body" - ), f"{cleanup} must run in the {command} layer" + ), 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)