diff --git a/truss/base/truss_config.py b/truss/base/truss_config.py index 7e03c73a9..d2ec98b90 100644 --- a/truss/base/truss_config.py +++ b/truss/base/truss_config.py @@ -1193,6 +1193,17 @@ class DockerServer(custom_types.ConfigModel): default=None, description="Skip the build step and deploy the base image as-is. Baseten copies the image to its container registry without running docker build or modifying the image in any way.", ) + max_payload_size: Optional[str] = pydantic.Field( + default=None, + description="The maximum inbound request body size for the container's reverse proxy. Accepts nginx size syntax, such as 64M, 128M, or 1G. Defaults to 64M.", + ) + + @pydantic.field_validator("max_payload_size") + @classmethod + def _validate_max_payload_size(cls, v: Optional[str]) -> Optional[str]: + if v is not None and not re.fullmatch(r"[0-9]+[kKmMgG]?", v): + raise ValueError(f"Invalid max_payload_size {v!r}.") + return v @pydantic.field_validator("run_as_user_id") @classmethod diff --git a/truss/config.schema.json b/truss/config.schema.json index 1e60c0b02..de51ef763 100644 --- a/truss/config.schema.json +++ b/truss/config.schema.json @@ -415,6 +415,19 @@ "default": null, "description": "Skip the build step and deploy the base image as-is. Baseten copies the image to its container registry without running docker build or modifying the image in any way.", "title": "No Build" + }, + "max_payload_size": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The maximum inbound request body size for the container's reverse proxy. Accepts nginx size syntax, such as 64M, 128M, or 1G. Defaults to 64M.", + "title": "Max Payload Size" } }, "required": [ diff --git a/truss/contexts/image_builder/serving_image_builder.py b/truss/contexts/image_builder/serving_image_builder.py index e081e342b..ec71ad890 100644 --- a/truss/contexts/image_builder/serving_image_builder.py +++ b/truss/contexts/image_builder/serving_image_builder.py @@ -458,7 +458,9 @@ def generate_docker_server_nginx_config(build_dir, config): readiness_endpoint=config.docker_server.readiness_endpoint, liveness_endpoint=config.docker_server.liveness_endpoint, server_port=config.docker_server.server_port, - client_max_body_size=TRUSSLESS_MAX_PAYLOAD_SIZE, + client_max_body_size=config.docker_server.max_payload_size + if config.docker_server.max_payload_size is not None + else TRUSSLESS_MAX_PAYLOAD_SIZE, transport_kind=config.runtime.transport.kind, ) nginx_filepath = build_dir / "proxy.conf" diff --git a/truss/tests/contexts/image_builder/test_serving_image_builder.py b/truss/tests/contexts/image_builder/test_serving_image_builder.py index 38ded38e6..9dcb63e8f 100644 --- a/truss/tests/contexts/image_builder/test_serving_image_builder.py +++ b/truss/tests/contexts/image_builder/test_serving_image_builder.py @@ -1033,7 +1033,12 @@ class MockConfig: assert cfg.has_section("eventlistener:quit_on_failure") -def test_nginx_config_disables_disk_writes(tmp_path): +@pytest.mark.parametrize( + "max_payload_size,expected_max_payload_size", [(None, "64M"), ("128M", "128M")] +) +def test_nginx_config_disables_disk_writes( + tmp_path, max_payload_size, expected_max_payload_size +): """Test that nginx configuration disables all disk writes.""" class MockDockerServer: @@ -1041,6 +1046,9 @@ class MockDockerServer: readiness_endpoint = "/readiness" liveness_endpoint = "/health" server_port = 8090 + max_payload_size = None + + MockDockerServer.max_payload_size = max_payload_size class MockTransport: kind = "http" @@ -1060,6 +1068,7 @@ class MockConfig: # Verify logging is disabled assert "access_log off;" in nginx_config assert "error_log /dev/null;" in nginx_config + assert f"client_max_body_size {expected_max_payload_size};" in nginx_config # Verify temp paths use /dev/shm (in-memory filesystem) assert "client_body_temp_path /dev/shm/nginx_client_temp;" in nginx_config diff --git a/truss/tests/test_config.py b/truss/tests/test_config.py index 25fc8b80e..b1c942a37 100644 --- a/truss/tests/test_config.py +++ b/truss/tests/test_config.py @@ -1421,6 +1421,20 @@ def test_docker_server_run_as_user_id(run_as_user_id, expected, raises): assert docker_server.run_as_user_id == expected +def test_docker_server_invalid_max_payload_size(): + with pytest.raises( + pydantic.ValidationError, match="Invalid max_payload_size '64MB'" + ): + DockerServer( + start_command="python main.py", + server_port=8000, + predict_endpoint="/predict", + readiness_endpoint="/health", + liveness_endpoint="/health", + max_payload_size="64MB", + ) + + # ============================================================================= # Weights Configuration Tests # =============================================================================