Repository: bentoml/BentoML
Pull request #5041 (2024-10-23 22:53:23)

docs: Add Base Hyperlint Config
## What does this PR address?

- This pull request adds the base Hyperlint configuration for Hyperlint's AI Reviewer.
- This pull request also adds the OpenAPI Monitoring for BentoML's cloud service.

cc @parano after our discussion on Monday.


Patch:
diff --git a/.hyperlint/config.yaml b/.hyperlint/config.yaml
new file mode 100644
index 00000000..e0bf327f
--- /dev/null
+++ b/.hyperlint/config.yaml
@@ -0,0 +1,5 @@
+content_dir: /docs/source
+openapi:
+  enabled: true
+  urls:
+    - https://cloud.bentoml.com/openapi.json
diff --git a/.hyperlint/styles/config/vocabularies/hyperlint/accept.txt b/.hyperlint/styles/config/vocabularies/hyperlint/accept.txt
new file mode 100644
index 00000000..4851b61d
--- /dev/null
+++ b/.hyperlint/styles/config/vocabularies/hyperlint/accept.txt
@@ -0,0 +1,2 @@
+BentoML
+BentoService
diff --git a/docs/source/bentocloud/byoc/aws.rst b/docs/source/bentocloud/byoc/aws.rst
index 12f9de1f..1fb9cd2f 100644
--- a/docs/source/bentocloud/byoc/aws.rst
+++ b/docs/source/bentocloud/byoc/aws.rst
@@ -4,6 +4,37 @@ BentoCloud BYOC Setup Guide for AWS
 
 As part of our commitment to ensuring a smooth and efficient onboarding process, we have prepared this setup guide to help your DevOps team integrate BentoCloud into your AWS account.
 
+Request quotas
+--------------
+
+To ensure there are no delays in your cluster setup, please make sure you have enough service quotas in your AWS account prior to starting the setup. If not, please request increased quotas in the region where you will deploy BentoCloud.
+
+To request quotas:
+
+1. Visit the `AWS Service Quotas console <https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas>`_ for your region.
+2. `Request quotas <https://docs.aws.amazon.com/servicequotas/latest/userguide/request-quota-increase.html>`_ as per your deployment plan. See the table below for quota details:
+
+   .. list-table::
+      :widths: 10 35 25 30
+      :header-rows: 1
+
+      * - Type
+        - Quota name
+        - Required quantity
+        - Purpose
+      * - CPU
+        - ``Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances``
+        - 32 vCPUs
+        - Run infrastructure workloads, image builder jobs, and CPU serving instances.
+      * - GPU
+        - Depending on needs:
+
+          - T4/A10G GPU: ``Running On-Demand G and VT instances``
+          - A100/H100 GPU: ``Running On-Demand P instances``
+
+        - Based on needs
+        - Run your workloads that require GPUs.
+
 Setup
 -----
 
diff --git a/docs/source/bentocloud/byoc/gcp.rst b/docs/source/bentocloud/byoc/gcp.rst
index 6c701a01..cc816acd 100644
--- a/docs/source/bentocloud/byoc/gcp.rst
+++ b/docs/source/bentocloud/byoc/gcp.rst
@@ -4,6 +4,40 @@ BentoCloud BYOC Setup Guide for GCP
 
 As part of our commitment to ensuring a smooth and efficient onboarding process, we have prepared this setup guide to help your DevOps team integrate BentoCloud into your GCP account.
 
+Request quotas
+--------------
+
+To ensure there are no delays in your cluster setup, please make sure you have enough service quotas in your GCP account prior to starting the setup. If not, please request increased quotas in the project and region where you will deploy BentoCloud.
+
+To request quotas:
+
+1. Visit the `GCP Quotas page <https://console.cloud.google.com/iam-admin/quotas>`_.
+2. `Request quotas <https://cloud.google.com/compute/resource-usage#vm_instance_quota>`_ in the correct project and region as per your deployment plan. See the table below for quota details:
+
+   .. list-table::
+      :widths: 10 35 25 30
+      :header-rows: 1
+
+      * - Type
+        - Quota name
+        - Required quantity
+        - Purpose
+      * - CPU
+        - ``CPUS``
+        - 32
+        - Run infrastructure workloads, image builder Pods, and serving instances.
+      * - GPU
+        - Depending on needs:
+
+          - T4: ``NVIDIA_T4_GPUS``
+          - L4: ``NVIDIA_L4_GPUS``
+          - A100 40GB: ``NVIDIA_A100_GPUS``
+          - A100 80GB: ``NVIDIA_A100_80GB_GPUS``
+          - H100 (Committed usage only): ``COMMITTED_NVIDIA_H100_GPUS``
+
+        - Based on needs
+        - Run your workloads that require GPUs.
+
 Setup
 -----
 
diff --git a/docs/source/guides/distributed-services.rst b/docs/source/guides/distributed-services.rst
index eaac7eb8..b46530d8 100644
--- a/docs/source/guides/distributed-services.rst
+++ b/docs/source/guides/distributed-services.rst
@@ -151,6 +151,65 @@ Once a dependency is declared, invoking methods on the dependent Service is simi
 
 Using ``bentoml.depends()`` is a recommended way for creating a BentoML project with distributed Services. It enhances modularity as you can develop reusable, loosely coupled Services that can be maintained and scaled independently.
 
+Depend on an external deployment
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+BentoML also allows you to set an external deployment as a dependency for a Service. This means the Service can call a remote model and its exposed API endpoints. To specify an external deployment, use the ``bentoml.depends()`` function, either by providing the deployment name on BentoCloud or the URL if it's already running.
+
+.. tab-set::
+
+   .. tab-item:: Specify the Deployment name on BentoCloud
+
+      You can also pass the ``cluster`` parameter to specify the cluster where your Deployment is running.
+
+      .. code-block:: python
+
+         import bentoml
+
+         @bentoml.service
+         class MyService:
+             # `cluster` is optional if your Deployment is in a non-default cluster
+             iris = bentoml.depends(deployment="iris-classifier-x6dewa", cluster="my_cluster_name")
+
+             @bentoml.api
+             def predict(self, input: np.ndarray) -> int:
+                 # Call the predict function from the remote Deployment
+                 return int(self.iris.predict(input)[0][0])
+
+   .. tab-item:: Specify the URL
+
+      If the external deployment is already running and its API is exposed via a public URL, you can reference it by specifying the ``url`` parameter. Note that ``url`` and ``deployment``/``cluster`` are mutually exclusive.
+
+      .. code-block:: python
+
+         import bentoml
+
+         @bentoml.service
+         class MyService:
+             # Call the model deployed on BentoCloud by specifying its URL
+             iris = bentoml.depends(url="https://<iris.example-url.bentoml.ai>")
+
+             # Call the model served elsewhere
+             # iris = bentoml.depends(url="http://192.168.1.1:3000")
+
+             @bentoml.api
+             def predict(self, input: np.ndarray) -> int:
+                 # Make a request to the external service hosted at the specified URL
+                 return int(self.iris.predict(input)[0][0])
+
+.. tip::
+
+   We recommend you specify the class of the external Service when using ``bentoml.depends()``. This makes it easier to validate the types and methods available on the remote Service.
+
+   .. code-block:: python
+
+      import bentoml
+
+      @bentoml.service
+      class MyService:
+          # Specify the external Service class for type-safe integration
+          iris = bentoml.depends(IrisClassifier, deployment="iris-classifier-x6dewa", cluster="my_cluster")
+
 ``bentofile.yaml``
 ------------------
 
diff --git a/src/_bentoml_impl/docker.py b/src/_bentoml_impl/docker.py
new file mode 100644
index 00000000..7a9f0477
--- /dev/null
+++ b/src/_bentoml_impl/docker.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+import importlib
+import os
+import shlex
+import typing as t
+
+import attrs
+from fs.base import FS
+from jinja2 import Environment
+from jinja2 import FileSystemLoader
+
+from bentoml._internal.bento.bento import ImageInfo
+from bentoml._internal.container.generate import DEFAULT_BENTO_ENVS
+from bentoml._internal.container.generate import PREHEAT_PIP_PACKAGES
+from bentoml._internal.container.generate import expands_bento_path
+from bentoml._internal.container.generate import resolve_package_versions
+from bentoml._internal.container.generate import to_bento_field
+from bentoml._internal.container.generate import to_options_field
+
+
+def get_templates_variables(
+    image: ImageInfo, bento_fs: FS, **bento_envs: t.Any
+) -> dict[str, t.Any]:
+    from bentoml._internal.configuration.containers import BentoMLContainer
+
+    bento_envs = {**DEFAULT_BENTO_ENVS, **bento_envs}
+    options = attrs.asdict(image)
+    requirement_file = bento_fs.getsyspath("env/python/requirements.txt")
+    if os.path.exists(requirement_file):
+        python_packages = resolve_package_versions(requirement_file)
+    else:
+        python_packages = {}
+    pip_preheat_packages = [
+        python_packages[k] for k in PREHEAT_PIP_PACKAGES if k in python_packages
+    ]
+    return {
+        **{to_options_field(k): v for k, v in options.items()},
+        **{to_bento_field(k): v for k, v in bento_envs.items()},
+        "__prometheus_port__": BentoMLContainer.grpc.metrics.port.get(),
+        "__pip_preheat_packages__": pip_preheat_packages,
+    }
+
+
+def generate_dockerfile(
+    image: ImageInfo, bento_fs: FS, *, frontend: str = "dockerfile", **bento_envs: t.Any
+) -> str:
+    templates_path = importlib.import_module(
+        f"bentoml._internal.container.frontend.{frontend}.templates"
+    ).__path__
+    environment = Environment(
+        extensions=["jinja2.ext.do", "jinja2.ext.loopcontrols", "jinja2.ext.debug"],
+        trim_blocks=True,
+        lstrip_blocks=True,
+        loader=FileSystemLoader(templates_path, followlinks=True),
+    )
+    environment.filters["bash_quote"] = shlex.quote
+    environment.globals["expands_bento_path"] = expands_bento_path
+    template = environment.get_template("base_v2.j2")
+    return template.render(**get_templates_variables(image, bento_fs, **bento_envs))
diff --git a/src/_bentoml_impl/loader.py b/src/_bentoml_impl/loader.py
index 976ed1db..2dd6911a 100644
--- a/src/_bentoml_impl/loader.py
+++ b/src/_bentoml_impl/loader.py
@@ -10,14 +10,19 @@ import typing as t
 
 import yaml
 
+from bentoml._internal.bento.bento import BENTO_YAML_FILENAME
+from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILES
 from bentoml._internal.context import server_context
 
+if sys.version_info >= (3, 11):
+    import tomllib
+else:
+    import tomli as tomllib
+
+
 if t.TYPE_CHECKING:
     from _bentoml_sdk import Service
 
-BENTO_YAML_FILENAME = "bento.yaml"
-BENTO_BUILD_CONFIG_FILENAME = "bentofile.yaml"
-
 
 def normalize_identifier(
     service_identifier: str,
@@ -51,23 +56,32 @@ def normalize_identifier(
             # this is a bento directory
             yaml_path = path.joinpath(BENTO_YAML_FILENAME)
             bento_path = path.joinpath("src")
-        elif path.is_file() and path.name == "bentofile.yaml":
-            # this is a bentofile.yaml file
+        elif path.is_file() and path.name in DEFAULT_BENTO_BUILD_FILES:
+            # this is a bento build config file
             yaml_path = path
             bento_path = (
                 pathlib.Path(working_dir) if working_dir is not None else path.parent
             )
-        elif path.is_dir() and path.joinpath("bentofile.yaml").is_file():
+        elif path.is_dir() and any(
+            path.joinpath(filename).is_file() for filename in DEFAULT_BENTO_BUILD_FILES
+        ):
             # this is a bento project directory
-            yaml_path = path.joinpath("bentofile.yaml")
-            bento_path = (
-                pathlib.Path(working_dir) if working_dir is not None else path.parent
+            yaml_path = next(
+                path.joinpath(filename)
+                for filename in DEFAULT_BENTO_BUILD_FILES
+                if path.joinpath(filename).is_file()
             )
+            bento_path = pathlib.Path(working_dir) if working_dir is not None else path
         else:
             raise ValueError(f"found a path but not a bento: {service_identifier}")
 
-        with open(yaml_path, "r") as f:
-            bento_yaml = yaml.safe_load(f)
+        if yaml_path.name == "pyproject.toml":
+            with yaml_path.open("rb") as f:
+                data = tomllib.load(f)
+            bento_yaml = data.get("tool", {}).get("bentoml", {}).get("build", {})
+        else:
+            with open(yaml_path, "r") as f:
+                bento_yaml = yaml.safe_load(f)
         assert "service" in bento_yaml, "service field is required in bento.yaml"
         return normalize_package(bento_yaml["service"]), bento_path
 
@@ -153,12 +167,13 @@ def import_service(
     if bento_path.with_name(BENTO_YAML_FILENAME).exists():
         bento = Bento.from_path(str(bento_path.parent))
         model_aliases = {m.alias: str(m.tag) for m in bento.info.all_models if m.alias}
-    elif (bentofile := bento_path.joinpath(BENTO_BUILD_CONFIG_FILENAME)).exists():
-        with open(bentofile, encoding="utf-8") as f:
-            build_config = BentoBuildConfig.from_yaml(f)
-        model_aliases = build_config.model_aliases
     else:
         model_aliases = {}
+        for filename in DEFAULT_BENTO_BUILD_FILES:
+            if (bentofile := bento_path.joinpath(filename)).exists():
+                build_config = BentoBuildConfig.from_file(bentofile)
+                model_aliases = build_config.model_aliases
+                break
     BentoMLContainer.model_aliases.set(model_aliases)
 
     try:
diff --git a/src/_bentoml_sdk/images.py b/src/_bentoml_sdk/images.py
new file mode 100644
index 00000000..9d409b4a
--- /dev/null
+++ b/src/_bentoml_sdk/images.py
@@ -0,0 +1,239 @@
+from __future__ import annotations
+
+import logging
+import platform
+import subprocess
+import sys
+import typing as t
+from pathlib import Path
+
+import attrs
+
+from bentoml._internal.bento.bento import ImageInfo
+from bentoml._internal.bento.build_config import BentoBuildConfig
+from bentoml._internal.configuration import get_bentoml_requirement
+from bentoml._internal.configuration import get_debug_mode
+from bentoml._internal.configuration import get_quiet_mode
+from bentoml._internal.container.frontend.dockerfile import CONTAINER_METADATA
+from bentoml._internal.container.frontend.dockerfile import CONTAINER_SUPPORTED_DISTROS
+from bentoml.exceptions import BentoMLConfigException
+from bentoml.exceptions import BentoMLException
+
+logger = logging.getLogger("bentoml.build")
+
+DEFAULT_PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
+BENTOML_GIT_DEPENDENCY = "bentoml @ git+https://github.com/bentoml/bentoml.git"
+
+
+@attrs.define
+class Image:
+    """A class defining the environment requirements for bento."""
+
+    base_image: str
+    python_version: str = DEFAULT_PYTHON_VERSION
+    commands: t.List[str] = attrs.field(factory=list)
+    lock_python_packages: bool = True
+    python_requirements: str = ""
+
+    def requirements_file(self, file_path: str) -> t.Self:
+        """Add a requirements file to the image. Supports chaining call.
+
+        Example:
+
+        .. code-block:: python
+
+            image = Image("debian:latest").requirements_file("requirements.txt")
+        """
+        self.python_requirements += Path(file_path).read_text()
+        return self
+
+    def python_packages(self, *packages: str) -> t.Self:
+        """Add python dependencies to the image. Supports chaining call.
+
+        Example:
+
+        .. code-block:: python
+
+            image = Image("debian:latest")\
+                .python_packages("numpy", "pandas")\
+                .requirements_file("requirements.txt")
+        """
+        self.python_requirements += "\n".join(packages)
+        return self
+
+    def run(self, command: str) -> t.Self:
+        """Add a command to the image. Supports chaining call.
+
+        Example:
+
+        .. code-block:: python
+
+            image = Image("debian:latest").run("echo 'Hello, World!'")
+        """
+        self.commands.append(command)
+        return self
+
+    def freeze(self, platform_: str | None = None) -> ImageInfo:
+        """Freeze the image to an ImageInfo object for build."""
+        if not self.lock_python_packages:
+            python_requirements = self.python_requirements
+        else:
+            python_requirements = self._freeze_python_requirements(platform_)
+        return ImageInfo(
+            base_image=self.base_image,
+            python_version=self.python_version,
+            commands=self.commands,
+            python_requirements=python_requirements,
+        )
+
+    def _freeze_python_requirements(self, platform_: str | None = None) -> str:
+        from tempfile import TemporaryDirectory
+
+        from pip_requirements_parser import RequirementsFile
+
+        with TemporaryDirectory(prefix="bento-reqs-") as parent:
+            requirements_in = Path(parent).joinpath("requirements.in")
+            requirements_in.write_text(self.python_requirements)
+            # XXX: RequirementsFile.from_string() does not work due to bugs
+            requirements_file = RequirementsFile.from_file(str(requirements_in))
+            has_bentoml_req = any(
+                req.name and req.name.lower() == "bentoml"
+                for req in requirements_file.requirements
+            )
+            with requirements_in.open("w") as f:
+                f.write(requirements_file.dumps(preserve_one_empty_line=True))
+                if not has_bentoml_req:
+                    req = get_bentoml_requirement() or BENTOML_GIT_DEPENDENCY
+                    f.write(f"{req}\n")
+            lock_args = [
+                str(requirements_in),
+                "--allow-unsafe",
+                "--no-header",
+                f"--output-file={requirements_in.with_suffix('.lock')}",
+                "--emit-index-url",
+                "--emit-find-links",
+                "--no-annotate",
+            ]
+            if get_debug_mode():
+                lock_args.append("--verbose")
+            else:
+                lock_args.append("--quiet")
+            logger.info("Locking PyPI package versions.")
+            if platform_:
+                lock_args.extend(["--python-platform", platform_])
+            elif platform.system() != "Linux" or platform.machine() != "x86_64":
+                logger.info(
+                    "Locking packages for x86_64-unknown-linux-gnu. "
+                    "Pass `--platform` option to specify the platform."
+                )
+                lock_args.extend(["--python-platform", "linux"])
+            cmd = [sys.executable, "-m", "uv", "pip", "compile", *lock_args]
+            try:
+                subprocess.check_call(
+                    cmd,
+                    text=True,
+                    stderr=subprocess.DEVNULL if get_quiet_mode() else None,
+                    cwd=parent,
+                )
+            except subprocess.CalledProcessError as e:
+                raise BentoMLException(f"Failed to lock PyPI packages: {e}") from None
+            return requirements_in.with_suffix(".lock").read_text()
+
+
+@attrs.define
+class PythonImage(Image):
+    base_image: str = ""
+    distro: str = "debian"
+    _original_base_image: str = attrs.field(init=False, default="")
+
+    def __attrs_post_init__(self) -> None:
+        self._original_base_image = self.base_image
+        if not self.base_image:
+            if self.distro not in CONTAINER_METADATA:
+                raise BentoMLConfigException(
+                    f"Unsupported distro: {self.distro}, expected one of {CONTAINER_SUPPORTED_DISTROS}"
+                )
+            python_version = self.python_version
+            if self.distro in ("ubi8",):
+                python_version = python_version.replace(".", "")
+            self.base_image = CONTAINER_METADATA[self.distro]["python"]["image"].format(
+                spec_version=python_version
+            )
+
+    def system_packages(self, *packages: str) -> t.Self:
+        if self._original_base_image:
+            raise BentoMLConfigException(
+                "system_packages() can only be used in default base image"
+            )
+        if self.distro not in CONTAINER_METADATA:
+            raise BentoMLConfigException(
+                f"Unsupported distro: {self.distro}, expected one of {CONTAINER_SUPPORTED_DISTROS}"
+            )
+        self.commands.append(
+            CONTAINER_METADATA[self.distro]["install_command"].format(
+                packages=" ".join(packages)
+            )
+        )
+        return self
+
+
+def get_image_from_build_config(build_config: BentoBuildConfig) -> Image | None:
+    if not build_config.conda.is_empty():
+        logger.warning(
+            "conda options are not supported by bento v2, fallback to bento v1"
+        )
+        return None
+    image = PythonImage()
+    docker_options = build_config.docker
+    if docker_options.cuda_version is not None:
+        logger.warning(
+            "docker.cuda_version is not supported by bento v2, fallback to bento v1"
+        )
+        return None
+    if docker_options.dockerfile_template is not None:
+        logger.warning(
+            "docker.dockerfile_template is not supported by bento v2, fallback to bento v1"
+        )
+        return None
+    if docker_options.setup_script is not None:
+        logger.warning(
+            "docker.setup_script is not supported by bento v2, fallback to bento v1"
+        )
+        return None
+    if docker_options.base_image is not None:
+        image.base_image = docker_options.base_image
+    if docker_options.distro is not None:
+        image.distro = docker_options.distro
+    if docker_options.python_version is not None:
+        image.python_version = docker_options.python_version
+    if docker_options.system_packages:
+        image.system_packages(*docker_options.system_packages)
+
+    python_options = build_config.python.with_defaults()
+    if python_options.wheels:
+        logger.warning(
+            "python.wheels is not supported by bento v2, fallback to bento v1"
+        )
+        return None
+    image.lock_python_packages = python_options.lock_packages
+    if python_options.index_url:
+        image.python_packages(f"--index-url {python_options.index_url}")
+    if python_options.no_index:
+        image.python_packages("--no-index")
+    if python_options.trusted_host:
+        image.python_packages(
+            *(f"--trusted-host {h}" for h in python_options.trusted_host)
+        )
+    if python_options.extra_index_url:
+        image.python_packages(
+            *(f"--extra-index-url {url}" for url in python_options.extra_index_url)
+        )
+    if python_options.find_links:
+        image.python_packages(
+            *(f"--find-links {link}" for link in python_options.find_links)
+        )
+    if python_options.requirements_txt:
+        image.requirements_file(python_options.requirements_txt)
+    if python_options.packages:
+        image.python_packages(*python_options.packages)
+    return image
diff --git a/src/_bentoml_sdk/service/factory.py b/src/_bentoml_sdk/service/factory.py
index 382ac1d3..5654bcf3 100644
--- a/src/_bentoml_sdk/service/factory.py
+++ b/src/_bentoml_sdk/service/factory.py
@@ -19,6 +19,7 @@ from typing_extensions import Unpack
 
 from bentoml import Runner
 from bentoml._internal.bento.bento import Bento
+from bentoml._internal.bento.build_config import BentoEnvSchema
 from bentoml._internal.configuration.containers import BentoMLContainer
 from bentoml._internal.context import ServiceContext
 from bentoml._internal.models import Model as StoredModel
@@ -27,6 +28,7 @@ from bentoml._internal.utils import dict_filter_none
 from bentoml.exceptions import BentoMLConfigException
 from bentoml.exceptions import BentoMLException
 
+from ..images import Image
 from ..method import APIMethod
 from ..models import BentoModel
 from ..models import HuggingFaceModel
@@ -61,13 +63,18 @@ def with_config(
     return wrapper
 
 
+def convert_envs(envs: t.List[t.Dict[str, t.Any]]) -> t.List[BentoEnvSchema]:
+    return [BentoEnvSchema(**env) for env in envs]
+
+
 @attrs.define
 class Service(t.Generic[T]):
     """A Bentoml service that can be served by BentoML server."""
 
     config: Config
     inner: type[T]
-
+    image: t.Optional[Image] = None
+    envs: t.List[BentoEnvSchema] = attrs.field(factory=list, converter=convert_envs)
     bento: t.Optional[Bento] = attrs.field(init=False, default=None)
     models: list[Model[t.Any]] = attrs.field(factory=list)
     apis: dict[str, APIMethod[..., t.Any]] = attrs.field(factory=dict)
@@ -406,10 +413,24 @@ def service(inner: type[T], /) -> Service[T]: ...
 
 
 @t.overload
-def service(inner: None = ..., /, **kwargs: Unpack[Config]) -> _ServiceDecorator: ...
-
-
-def service(inner: type[T] | None = None, /, **kwargs: Unpack[Config]) -> t.Any:
+def service(
+    inner: None = ...,
+    /,
+    *,
+    image: Image | None = None,
+    envs: list[dict[str, t.Any]] | None = None,
+    **kwargs: Unpack[Config],
+) -> _ServiceDecorator: ...
+
+
+def service(
+    inner: type[T] | None = None,
+    /,
+    *,
+    image: Image | None = None,
+    envs: list[dict[str, t.Any]] | None = None,
+    **kwargs: Unpack[Config],
+) -> t.Any:
     """Mark a class as a BentoML service.
 
     Example:
@@ -425,7 +446,7 @@ def service(inner: type[T] | None = None, /, **kwargs: Unpack[Config]) -> t.Any:
     def decorator(inner: type[T]) -> Service[T]:
         if isinstance(inner, Service):
             raise TypeError("service() decorator can only be applied once")
-        return Service(config=config, inner=inner)
+        return Service(config=config, inner=inner, image=image, envs=envs or [])
 
     return decorator(inner) if inner is not None else decorator
 
diff --git a/src/bentoml/__init__.py b/src/bentoml/__init__.py
index 3613c6e9..6fbcdf35 100644
--- a/src/bentoml/__init__.py
+++ b/src/bentoml/__init__.py
@@ -106,6 +106,7 @@ if TYPE_CHECKING:
     from _bentoml_sdk import api
     from _bentoml_sdk import depends
     from _bentoml_sdk import get_current_service
+    from _bentoml_sdk import images
     from _bentoml_sdk import mount_asgi_app
     from _bentoml_sdk import on_deployment
     from _bentoml_sdk import on_shutdown
@@ -233,6 +234,7 @@ else:
     models = _LazyLoader("bentoml.models", globals(), "bentoml.models")
     metrics = _LazyLoader("bentoml.metrics", globals(), "bentoml.metrics")
     container = _LazyLoader("bentoml.container", globals(), "bentoml.container")
+    images = _LazyLoader("bentoml.images", globals(), "bentoml.images")
     client = _LazyLoader("bentoml.client", globals(), "bentoml.client")
     server = _LazyLoader("bentoml.server", globals(), "bentoml.server")
     exceptions = _LazyLoader("bentoml.exceptions", globals(), "bentoml.exceptions")
@@ -353,6 +355,7 @@ __all__ = [
     "runner_service",
     "api",
     "task",
+    "images",
     "on_shutdown",
     "on_deployment",
     "depends",
diff --git a/src/bentoml/_internal/bento/bento.py b/src/bentoml/_internal/bento/bento.py
index 3bfa2cb9..3022cc1f 100644
--- a/src/bentoml/_internal/bento/bento.py
+++ b/src/bentoml/_internal/bento/bento.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import inspect
 import json
 import logging
 import os
@@ -67,7 +66,7 @@ logger = logging.getLogger(__name__)
 BENTO_YAML_FILENAME = "bento.yaml"
 BENTO_PROJECT_DIR_NAME = "src"
 BENTO_README_FILENAME = "README.md"
-DEFAULT_BENTO_BUILD_FILE = "bentofile.yaml"
+DEFAULT_BENTO_BUILD_FILES = ("bentofile.yaml", "pyproject.toml")
 
 API_INFO_MD = "| POST [`/{api}`](#{link}) | {input} | {output} |"
 
@@ -143,7 +142,7 @@ class Bento(StoreItem):
     _tag: Tag = attr.field()
     __fs: FS = attr.field()
 
-    _info: BentoInfo
+    _info: BaseBentoInfo
 
     _model_store: ModelStore | None = None
     _doc: str | None = None
@@ -176,7 +175,7 @@ class Bento(StoreItem):
         return self.__fs
 
     @property
-    def info(self) -> BentoInfo:
+    def info(self) -> BaseBentoInfo:
         return self._info
 
     @property
@@ -190,23 +189,28 @@ class Bento(StoreItem):
 
         info = self.info
         models = [str(m.tag) for m in info.all_models]
-        runners = [
-            BentoRunnerSchema(
-                name=r.name,
-                runnable_type=r.runnable_type,
-                models=r.models,
-                resource_config=(
-                    BentoRunnerResourceSchema(
-                        cpu=r.resource_config.get("cpu"),
-                        nvidia_gpu=r.resource_config.get("nvidia.com/gpu"),
-                        custom_resources=r.resource_config.get("custom_resources"),
-                    )
-                    if r.resource_config
-                    else None
-                ),
-            )
-            for r in info.runners
-        ]
+        if isinstance(info, BentoInfo):
+            runners = [
+                BentoRunnerSchema(
+                    name=r.name,
+                    runnable_type=r.runnable_type,
+                    models=r.models,
+                    resource_config=(
+                        BentoRunnerResourceSchema(
+                            cpu=r.resource_config.get("cpu"),
+                            nvidia_gpu=r.resource_config.get("nvidia.com/gpu"),
+                            custom_resources=r.resource_config.get("custom_resources"),
+                        )
+                        if r.resource_config
+                        else None
+                    ),
+                )
+                for r in info.runners
+            ]
+            image: ImageInfo | None = None
+        else:
+            image = t.cast(ImageInfo, info.image)
+            runners = []
         return BentoManifestSchema(
             name=info.name,
             entry_service=info.entry_service,
@@ -220,6 +224,7 @@ class Bento(StoreItem):
             envs=info.envs,
             schema=info.schema,
             version=info.version,
+            image=image,
         )
 
     @classmethod
@@ -232,7 +237,10 @@ class Bento(StoreItem):
         platform: t.Optional[str] = None,
         bare: bool = False,
         reload: bool = False,
+        enabled_features: list[str] = Provide[BentoMLContainer.enabled_features],
     ) -> Bento:
+        from _bentoml_sdk.images import Image
+        from _bentoml_sdk.images import get_image_from_build_config
         from _bentoml_sdk.models import BentoModel
 
         from ..service import Service
@@ -243,6 +251,7 @@ class Bento(StoreItem):
             if build_ctx is None
             else os.path.realpath(os.path.expanduser(build_ctx))
         )
+        enable_image = "bento_image" in enabled_features
         if not os.path.isdir(build_ctx):
             raise InvalidArgument(
                 f"Bento build context {build_ctx} does not exist or is not a directory."
@@ -258,7 +267,7 @@ class Bento(StoreItem):
         )
         is_legacy = isinstance(svc, Service)
         # Apply default build options
-        build_config = build_config.with_defaults()
+        image: Image | None = None
 
         if isinstance(svc, Service):
             # for < 1.2
@@ -273,6 +282,18 @@ class Bento(StoreItem):
                 if build_config.name is not None
                 else to_snake_case(svc.name)
             )
+            build_config.envs.extend(svc.envs)
+            if enable_image:
+                image = svc.image
+            elif svc.image is not None:
+                logger.warning(
+                    "BentoML service %s has an image config, but BentoML image feature is not enabled. "
+                    "Please enable it by setting BENTOML_ENABLE_FEATURES=bento_image",
+                    svc.name,
+                )
+        if image is None and enable_image:
+            image = get_image_from_build_config(build_config)
+        build_config = build_config.with_defaults()
         tag = Tag(bento_name, version)
         if version is None:
             tag = tag.make_new_version()
@@ -318,7 +339,7 @@ class Bento(StoreItem):
                 )
             bento_fs.makedir(BENTO_PROJECT_DIR_NAME)
             target_fs = bento_fs.opendir(BENTO_PROJECT_DIR_NAME)
-            with target_fs.open(DEFAULT_BENTO_BUILD_FILE, "w") as bentofile_yaml:
+            with target_fs.open("bentofile.yaml", "w") as bentofile_yaml:
                 build_config.to_yaml(bentofile_yaml)
 
             for dir_path, _, files in ctx_fs.walk():
@@ -329,12 +350,16 @@ class Bento(StoreItem):
                             logger.warn("File size is larger than 10MiB: %s", path)
                         target_fs.makedirs(dir_path, recreate=True)
                         copy_file(ctx_fs, path, target_fs, path)
-
-            # NOTE: we need to generate both Python and Conda
-            # first to make sure we can generate the Dockerfile correctly.
-            build_config.python.write_to_bento(bento_fs, build_ctx, platform_=platform)
-            build_config.conda.write_to_bento(bento_fs, build_ctx)
-            build_config.docker.write_to_bento(bento_fs, build_ctx, build_config.conda)
+            if image is None:
+                # NOTE: we need to generate both Python and Conda
+                # first to make sure we can generate the Dockerfile correctly.
+                build_config.python.write_to_bento(
+                    bento_fs, build_ctx, platform_=platform
+                )
+                build_config.conda.write_to_bento(bento_fs, build_ctx)
+                build_config.docker.write_to_bento(
+                    bento_fs, build_ctx, build_config.conda
+                )
 
             # Create `readme.md` file
             if build_config.description is None:
@@ -358,10 +383,8 @@ class Bento(StoreItem):
                 with bento_fs.open(fs.path.combine("apis", "schema.json"), "w") as f:
                     json.dump(svc.schema(), f, indent=2)
 
-        res = Bento(
-            tag,
-            bento_fs,
-            BentoInfo(
+        if image is None:
+            bento_info = BentoInfo(
                 tag=tag,
                 service=svc,  # type: ignore # attrs converters do not typecheck
                 entry_service=svc.name,
@@ -390,8 +413,29 @@ class Bento(StoreItem):
                 conda=build_config.conda,
                 envs=build_config.envs,
                 schema=svc.schema() if not is_legacy else {},
-            ),
-        )
+            )
+        else:
+            bento_info = BentoInfoV2(
+                tag=tag,
+                service=svc,  # type: ignore # attrs converters do not typecheck
+                entry_service=svc.name,
+                labels=build_config.labels,
+                models=models,
+                services=(
+                    [
+                        BentoServiceInfo.from_service(s)
+                        for s in svc.all_services().values()
+                    ]
+                    if not is_legacy
+                    else []
+                ),
+                envs=build_config.envs,
+                schema=svc.schema() if not is_legacy else {},
+                image=image.freeze(platform),
+            )
+            bento_info.image.write_to_bento(bento_fs, build_config.envs)
+
+        res = Bento(tag, bento_fs, bento_info)
         if bare:
             return res
         # Create bento.yaml
@@ -407,7 +451,7 @@ class Bento(StoreItem):
     def from_fs(cls, item_fs: FS) -> Bento:
         try:
             with item_fs.open(BENTO_YAML_FILENAME, "r", encoding="utf-8") as bento_yaml:
-                info = BentoInfo.from_yaml_file(bento_yaml)
+                info = BaseBentoInfo.from_yaml_file(bento_yaml)
         except fs.errors.ResourceNotFound:
             raise BentoMLException(
                 f"Failed to load bento because it does not contain a '{BENTO_YAML_FILENAME}'"
@@ -659,7 +703,7 @@ def get_service_import_str(svc: Service | NewService[t.Any] | str) -> str:
 
 
 @attr.frozen(repr=False)
-class BentoInfo:
+class BaseBentoInfo:
     # for backward compatibility in case new fields are added to BentoInfo.
     __forbid_extra_keys__ = False
     # omit field in yaml file if it is not provided by the user.
@@ -677,18 +721,12 @@ class BentoInfo:
         factory=dict, converter=normalize_labels_value
     )
     models: t.List[BentoModelInfo] = attr.field(factory=list)
-    runners: t.List[BentoRunnerInfo] = attr.field(factory=list)
     # for BentoML 1.2+ SDK
     entry_service: str = attr.field(factory=str)
     services: t.List[BentoServiceInfo] = attr.field(factory=list)
     envs: t.List[BentoEnvSchema] = attr.field(factory=list)
     schema: t.Dict[str, t.Any] = attr.field(factory=dict)
 
-    apis: t.List[BentoApiInfo] = attr.field(factory=list)
-    docker: DockerOptions = attr.field(factory=lambda: DockerOptions().with_defaults())
-    python: PythonOptions = attr.field(factory=lambda: PythonOptions().with_defaults())
-    conda: CondaOptions = attr.field(factory=lambda: CondaOptions().with_defaults())
-
     @property
     def all_models(self) -> t.List[BentoModelInfo]:
         model_map = {model.tag: model for model in self.models}
@@ -715,7 +753,7 @@ class BentoInfo:
         return yaml.safe_dump(self.to_dict(), stream, sort_keys=False)
 
     @classmethod
-    def from_yaml_file(cls, stream: t.IO[t.Any]) -> BentoInfo:
+    def from_yaml_file(cls, stream: t.IO[t.Any]) -> BaseBentoInfo:
         try:
             yaml_content = yaml.safe_load(stream)
         except yaml.YAMLError as exc:
@@ -752,8 +790,12 @@ class BentoInfo:
                         models,
                     )
                 )
+        if yaml_content.pop("spec", 1) == 2:
+            klass = BentoInfoV2
+        else:
+            klass = BentoInfo
         try:
-            return bentoml_cattr.structure(yaml_content, cls)
+            return bentoml_cattr.structure(yaml_content, klass)
         except KeyError as e:
             raise BentoMLException(f"Missing field {e} in {BENTO_YAML_FILENAME}")
 
@@ -762,6 +804,53 @@ class BentoInfo:
         ...
 
 
+@attr.frozen(repr=False)
+class BentoInfo(BaseBentoInfo):
+    spec: int = attr.field(default=1, init=False)
+    runners: t.List[BentoRunnerInfo] = attr.field(factory=list)
+    apis: t.List[BentoApiInfo] = attr.field(factory=list)
+    docker: DockerOptions = attr.field(factory=lambda: DockerOptions().with_defaults())
+    python: PythonOptions = attr.field(factory=lambda: PythonOptions().with_defaults())
+    conda: CondaOptions = attr.field(factory=lambda: CondaOptions().with_defaults())
+
+
+@attr.frozen(repr=False)
+class ImageInfo:
+    base_image: str = ""
+    python_version: str = ""
+    commands: t.List[str] = attr.field(factory=list)
+    python_requirements: str = ""
+
+    def write_to_bento(self, bento_fs: FS, envs: list[BentoEnvSchema]) -> None:
+        from importlib import resources
+
+        from _bentoml_impl.docker import generate_dockerfile
+
+        py_folder = fs.path.join("env", "python")
+        bento_fs.makedirs(py_folder, recreate=True)
+        reqs_txt = fs.path.join(py_folder, "requirements.txt")
+        bento_fs.writetext(reqs_txt, self.python_requirements)
+
+        docker_folder = fs.path.join("env", "docker")
+        bento_fs.makedirs(docker_folder, recreate=True)
+        dockerfile_path = fs.path.join(docker_folder, "Dockerfile")
+        bento_fs.writetext(
+            dockerfile_path,
+            generate_dockerfile(self, bento_fs, enable_buildkit=False, envs=envs),
+        )
+
+        with resources.path(
+            "bentoml._internal.container.frontend.dockerfile", "entrypoint.sh"
+        ) as entrypoint_path:
+            copy_file_to_fs_folder(str(entrypoint_path), bento_fs, docker_folder)
+
+
+@attr.frozen(repr=False)
+class BentoInfoV2(BaseBentoInfo):
+    spec: int = attr.field(default=2, init=False)
+    image: ImageInfo = attr.field(factory=ImageInfo)
+
+
 bentoml_cattr.register_unstructure_hook(
     BentoDependencyInfo,
     make_dict_unstructure_fn(
@@ -787,15 +876,6 @@ bentoml_cattr.register_structure_hook(
     BentoDependencyInfo, _convert_bento_dependency_info
 )
 
-bentoml_cattr.register_structure_hook_func(
-    lambda cls: inspect.isclass(cls) and issubclass(cls, BentoInfo),
-    make_dict_structure_fn(
-        BentoInfo,
-        bentoml_cattr,
-        name=override(omit=True),
-        version=override(omit=True),
-    ),
-)
 bentoml_cattr.register_unstructure_hook(
     BentoModelInfo,
     make_dict_unstructure_fn(
@@ -806,8 +886,17 @@ bentoml_cattr.register_unstructure_hook(
         metadata=override(omit_if_default=True),
     ),
 )
-bentoml_cattr.register_unstructure_hook(
-    BentoInfo,
+bentoml_cattr.register_structure_hook_factory(
+    lambda cls: issubclass(cls, BaseBentoInfo),
+    lambda cls: make_dict_structure_fn(
+        cls,
+        bentoml_cattr,
+        name=override(omit=True),
+        version=override(omit=True),
+    ),
+)
+bentoml_cattr.register_unstructure_hook_factory(
+    lambda cls: issubclass(cls, BaseBentoInfo),
     # Ignore tag, tag is saved via the name and version field
-    make_dict_unstructure_fn(BentoInfo, bentoml_cattr, tag=override(omit=True)),
+    lambda cls: make_dict_unstructure_fn(cls, bentoml_cattr, tag=override(omit=True)),
 )
diff --git a/src/bentoml/_internal/bento/build_config.py b/src/bentoml/_internal/bento/build_config.py
index 52ab3822..15efd9b2 100644
--- a/src/bentoml/_internal/bento/build_config.py
+++ b/src/bentoml/_internal/bento/build_config.py
@@ -10,6 +10,7 @@ import typing as t
 from sys import version_info
 
 import attr
+import cattrs
 import fs
 import fs.copy
 import jinja2
@@ -934,16 +935,58 @@ class BentoBuildConfig:
         except yaml.YAMLError as exc:
             logger.error(exc)
             raise
+        return cls.load(yaml_content)
 
+    @classmethod
+    def from_pyproject(cls, stream: t.BinaryIO) -> BentoBuildConfig:
+        if sys.version_info >= (3, 11):
+            import tomllib
+        else:
+            import tomli as tomllib
+        data = tomllib.load(stream)
+        config = data.get("tool", {}).get("bentoml", {}).get("build", {})
+        if "name" in data.get("project", {}):
+            config.setdefault("name", data["project"]["name"])
+        build_config = cls.load(config)
+        dependencies = data.get("project", {}).get("dependencies", {})
+        python_packages = build_config.python.packages or []
+        python_packages.extend(dependencies)
+        object.__setattr__(build_config.python, "packages", python_packages)
+        return build_config
+
+    @classmethod
+    def from_file(cls, path: str) -> BentoBuildConfig:
+        if os.path.basename(path) == "pyproject.toml":
+            with open(path, "rb") as f:
+                return cls.from_pyproject(f)
+        else:
+            with open(path, encoding="utf-8") as f:
+                return cls.from_yaml(f)
+
+    @classmethod
+    def load(cls, data: dict[str, t.Any]) -> BentoBuildConfig:
         try:
-            return bentoml_cattr.structure(yaml_content, cls)
-        except TypeError as e:
-            if "missing 1 required positional argument: 'service'" in str(e):
+            return bentoml_cattr.structure(data, cls)
+        except cattrs.errors.BaseValidationError as e:
+            if any(
+                isinstance(exc, KeyError) and exc.args[0] == "service"
+                for exc in e.exceptions
+            ):
                 raise InvalidArgument(
                     'Missing required build config field "service", which indicates import path of target bentoml.Service instance. e.g.: "service: fraud_detector.py:svc"'
-                ) from e
+                ) from None
             else:
-                raise InvalidArgument(str(e)) from e
+                raise
+
+    @classmethod
+    def from_bento_dir(cls, bento_dir: str) -> BentoBuildConfig:
+        from .bento import DEFAULT_BENTO_BUILD_FILES
+
+        for filename in DEFAULT_BENTO_BUILD_FILES:
+            bentofile_path = os.path.join(bento_dir, filename)
+            if os.path.exists(bentofile_path):
+                return cls.from_file(bentofile_path).with_defaults()
+        return cls(service="").with_defaults()
 
     def to_yaml(self, stream: t.TextIO) -> None:
         try:
@@ -1013,4 +1056,4 @@ class FilledBentoBuildConfig(BentoBuildConfig):
     python: PythonOptions
     conda: CondaOptions
     models: t.List[ModelSpec]
-    envs: t.List[t.Dict[str, str]]
+    envs: t.List[BentoEnvSchema]
diff --git a/src/bentoml/_internal/cloud/bento.py b/src/bentoml/_internal/cloud/bento.py
index f03bbfe9..1c79e426 100644
--- a/src/bentoml/_internal/cloud/bento.py
+++ b/src/bentoml/_internal/cloud/bento.py
@@ -24,11 +24,7 @@ from .base import UPLOAD_RETRY_COUNT
 from .base import CallbackIOWrapper
 from .base import Spinner
 from .model import ModelAPI
-from .schemas.modelschemas import BentoApiSchema
-from .schemas.modelschemas import BentoRunnerResourceSchema
-from .schemas.modelschemas import BentoRunnerSchema
 from .schemas.modelschemas import UploadStatus
-from .schemas.schemasv1 import BentoManifestSchema
 from .schemas.schemasv1 import BentoSchema
 from .schemas.schemasv1 import CompleteMultipartUploadSchema
 from .schemas.schemasv1 import CompletePartSchema
@@ -80,7 +76,6 @@ class BentoAPI:
             self._do_push_bento(
                 bento, upload_task_id, force=force, threads=threads, bare=bare
             )
-            self.spinner.log(f'✅ Pushed Bento "{bento.tag}"')
 
     @inject
     def _do_push_bento(
@@ -155,39 +150,7 @@ class BentoAPI:
         labels: list[LabelItemSchema] = [
             LabelItemSchema(key=key, value=value) for key, value in info.labels.items()
         ]
-        apis: dict[str, BentoApiSchema] = {}
-        models = [str(m.tag) for m in info.all_models]
-        runners = [
-            BentoRunnerSchema(
-                name=r.name,
-                runnable_type=r.runnable_type,
-                models=r.models,
-                resource_config=(
-                    BentoRunnerResourceSchema(
-                        cpu=r.resource_config.get("cpu"),
-                        nvidia_gpu=r.resource_config.get("nvidia.com/gpu"),
-                        custom_resources=r.resource_config.get("custom_resources"),
-                    )
-                    if r.resource_config
-                    else None
-                ),
-            )
-            for r in info.runners
-        ]
-        manifest = BentoManifestSchema(
-            name=info.name,
-            entry_service=info.entry_service,
-            service=info.service,
-            bentoml_version=info.bentoml_version,
-            apis=apis,
-            models=models,
-            runners=runners,
-            size_bytes=bento.total_size(),
-            services=info.services,
-            envs=info.envs,
-            schema=info.schema,
-            dev=bare,
-        )
+        manifest = bento.get_manifest()
         if not remote_bento:
             with self.spinner.spin(
                 text=f'Registering Bento "{bento.tag}" with remote Bento store..'
@@ -283,9 +246,13 @@ class BentoAPI:
                         data=io_with_cb,
                     )
                 except Exception as e:  # pylint: disable=broad-except
-                    self.spinner.log(f'[bold red]Failed to upload bento "{bento.tag}"')
+                    self.spinner.log(
+                        f'[bold red]🚨 Failed to upload bento "{bento.tag}"'
+                    )
                     raise e
-                self.spinner.log(f'[bold green]Successfully pushed bento "{bento.tag}"')
+                self.spinner.log(
+                    f'[bold green]✅ Successfully pushed bento "{bento.tag}"'
+                )
                 return
             finish_req = FinishUploadSchema(
                 status=UploadStatus.SUCCESS.value, reason=""
diff --git a/src/bentoml/_internal/cloud/deployment.py b/src/bentoml/_internal/cloud/deployment.py
index 47c941b7..7dd7649c 100644
--- a/src/bentoml/_internal/cloud/deployment.py
+++ b/src/bentoml/_internal/cloud/deployment.py
@@ -21,6 +21,7 @@ from rich.console import Console
 from simple_di import Provide
 from simple_di import inject
 
+from ..bento.bento import DEFAULT_BENTO_BUILD_FILES
 from ..bento.bento import Bento
 from ..bento.build_config import BentoBuildConfig
 from ..configuration import is_editable_bentoml
@@ -756,7 +757,7 @@ class Deployment:
         else:
             raise TimeoutError("Timeout waiting for API server pod to be ready")
 
-        build_config = get_bento_build_config(bento_dir)
+        build_config = BentoBuildConfig.from_bento_dir(bento_dir)
         bento_spec = BentoPathSpec(
             build_config.include, build_config.exclude, bento_dir
         )
@@ -768,7 +769,10 @@ class Deployment:
             for fn in files:
                 full_path = os.path.join(root, fn)
                 rel_path = os.path.relpath(full_path, bento_dir).replace(os.sep, "/")
-                if not bento_spec.includes(rel_path) and rel_path != "bentofile.yaml":
+                if (
+                    not bento_spec.includes(rel_path)
+                    and rel_path not in DEFAULT_BENTO_BUILD_FILES
+                ):
                     continue
                 if rel_path in (REQUIREMENTS_TXT, "setup.sh"):
                     continue
@@ -813,7 +817,7 @@ class Deployment:
         from .bento import BentoAPI
 
         bento_dir = os.path.abspath(bento_dir)
-        build_config = get_bento_build_config(bento_dir)
+        build_config = BentoBuildConfig.from_bento_dir(bento_dir)
         deployment_api = DeploymentAPI(self._client)
         bento_api = BentoAPI(self._client)
         bento_spec = BentoPathSpec(
@@ -836,7 +840,7 @@ class Deployment:
                 return EDITABLE_BENTOML_PATHSPEC.match_file(rel_path)
             rel_path = os.path.relpath(path, bento_dir)
             return rel_path in (
-                "bentofile.yaml",
+                *DEFAULT_BENTO_BUILD_FILES,
                 REQUIREMENTS_TXT,
                 "setup.sh",
             ) or bento_spec.includes(rel_path)
@@ -925,7 +929,7 @@ class Deployment:
                                     needs_update = True
                                     break
 
-                        build_config = get_bento_build_config(bento_dir)
+                        build_config = BentoBuildConfig.from_bento_dir(bento_dir)
                         upload_files: list[tuple[str, bytes]] = []
                         delete_files: list[str] = []
                         affected_files: set[str] = set()
@@ -1436,16 +1440,6 @@ class InstanceTypeInfo:
         return {k: v for k, v in attr.asdict(self).items() if v is not None and v != ""}
 
 
-def get_bento_build_config(bento_dir: str) -> BentoBuildConfig:
-    bentofile_path = os.path.join(bento_dir, "bentofile.yaml")
-    if not os.path.exists(bentofile_path):
-        return BentoBuildConfig(service="").with_defaults()
-    else:
-        # respect bentofile.yaml include and exclude
-        with open(bentofile_path, "r") as f:
-            return BentoBuildConfig.from_yaml(f).with_defaults()
-
-
 REQUIREMENTS_TXT = "requirements.txt"
 EDITABLE_BENTOML_DIR = "__editable_bentoml__"
 EDITABLE_BENTOML_PATHSPEC = PathSpec.from_lines(
diff --git a/src/bentoml/_internal/cloud/model.py b/src/bentoml/_internal/cloud/model.py
index e9c5dcbe..17a790ce 100644
--- a/src/bentoml/_internal/cloud/model.py
+++ b/src/bentoml/_internal/cloud/model.py
@@ -175,9 +175,13 @@ class ModelAPI:
                         data=io_with_cb,
                     )
                 except Exception as e:  # pylint: disable=broad-except
-                    self.spinner.log(f'[bold red]Failed to upload model "{model.tag}"')
+                    self.spinner.log(
+                        f'[bold red]🚨 Failed to upload model "{model.tag}"'
+                    )
                     raise e
-                self.spinner.log(f'[bold green]Successfully pushed model "{model.tag}"')
+                self.spinner.log(
+                    f'[bold green]✅ Successfully pushed model "{model.tag}"'
+                )
                 return
             finish_req = FinishUploadSchema(
                 status=UploadStatus.SUCCESS.value, reason=""
diff --git a/src/bentoml/_internal/cloud/schemas/modelschemas.py b/src/bentoml/_internal/cloud/schemas/modelschemas.py
index 4ef3e287..ec781a7e 100644
--- a/src/bentoml/_internal/cloud/schemas/modelschemas.py
+++ b/src/bentoml/_internal/cloud/schemas/modelschemas.py
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
 import attr
 
 from ...bento.bento import BentoServiceInfo
+from ...bento.bento import ImageInfo
 from ...bento.build_config import BentoEnvSchema
 from ...cloud.schemas.utils import dict_options_converter
 from ...tag import Tag
@@ -77,8 +78,8 @@ class BentoManifestSchema:
     service: str
     bentoml_version: str = attr.field(eq=False)
     size_bytes: int = attr.field(eq=False)
-    entry_service: str = attr.field(default="")
-    name: t.Optional[str] = attr.field(default=None)
+    entry_service: str = ""
+    name: t.Optional[str] = None
     apis: t.Dict[str, BentoApiSchema] = attr.field(factory=dict)
     models: t.List[str] = attr.field(factory=list, eq=False)
     runners: t.Optional[t.List[BentoRunnerSchema]] = attr.field(factory=list)
@@ -87,6 +88,7 @@ class BentoManifestSchema:
     schema: t.Dict[str, t.Any] = attr.field(factory=dict)
     version: t.Optional[str] = attr.field(default=None, eq=False)
     dev: bool = attr.field(default=False, eq=False)
+    image: t.Optional[ImageInfo] = None
 
     @property
     def tag(self) -> Tag:
diff --git a/src/bentoml/_internal/container/__init__.py b/src/bentoml/_internal/container/__init__.py
index 03492176..1746216d 100644
--- a/src/bentoml/_internal/container/__init__.py
+++ b/src/bentoml/_internal/container/__init__.py
@@ -145,7 +145,9 @@ def construct_containerfile(
     from _bentoml_sdk.models import BentoModel
     from _bentoml_sdk.models import HuggingFaceModel
 
+    from ..bento.bento import BaseBentoInfo
     from ..bento.bento import BentoInfo
+    from ..bento.bento import BentoInfoV2
     from ..bento.build_config import DockerOptions
 
     dockerfile_path = "env/docker/Dockerfile"
@@ -154,7 +156,7 @@ def construct_containerfile(
     with fs.open_fs("temp://") as temp_fs:
         tempdir = temp_fs.getsyspath("/")
         with open(bento.path_of("bento.yaml"), "rb") as bento_yaml:
-            options = BentoInfo.from_yaml_file(bento_yaml)
+            options = BaseBentoInfo.from_yaml_file(bento_yaml)
         # tmpdir is our new build context.
         fs.mirror.mirror(bento._fs, temp_fs, copy_if_newer=True)
 
@@ -173,37 +175,53 @@ def construct_containerfile(
         # Dockerfile inside bento, and it is not relevant to
         # construct_containerfile. Hence it is safe to set it to None here.
         # See https://github.com/bentoml/BentoML/issues/3399.
-        docker_attrs = bentoml_cattr.unstructure(options.docker)
-        if (
-            "dockerfile_template" in docker_attrs
-            and docker_attrs["dockerfile_template"] is not None
-        ):
-            # NOTE: if users specify a dockerfile_template, we will
-            # save it to /env/docker/Dockerfile.template. This is necessary
-            # for the reconstruction of the Dockerfile.
-            docker_attrs["dockerfile_template"] = "env/docker/Dockerfile.template"
-
-        dockerfile = generate_containerfile(
-            docker=DockerOptions(**docker_attrs),
-            build_ctx=tempdir,
-            conda=options.conda,
-            bento_fs=temp_fs,
-            enable_buildkit=enable_buildkit,
-            add_header=add_header,
-        )
-        instruction.append(dockerfile)
-        if features is not None:
-            diff = set(features).difference(FEATURES)
-            if len(diff) > 0:
-                raise InvalidArgument(
-                    f"Available features are: {FEATURES}. Invalid fields from provided: {diff}"
-                )
-            PIP_CACHE_MOUNT = (
-                "--mount=type=cache,target=/root/.cache/pip " if enable_buildkit else ""
+        if isinstance(options, BentoInfo):
+            docker_attrs = bentoml_cattr.unstructure(options.docker)
+            if (
+                "dockerfile_template" in docker_attrs
+                and docker_attrs["dockerfile_template"] is not None
+            ):
+                # NOTE: if users specify a dockerfile_template, we will
+                # save it to /env/docker/Dockerfile.template. This is necessary
+                # for the reconstruction of the Dockerfile.
+                docker_attrs["dockerfile_template"] = "env/docker/Dockerfile.template"
+
+            dockerfile = generate_containerfile(
+                docker=DockerOptions(**docker_attrs),
+                build_ctx=tempdir,
+                conda=options.conda,
+                bento_fs=temp_fs,
+                enable_buildkit=enable_buildkit,
+                add_header=add_header,
             )
-            instruction.append(
-                "RUN %spip install bentoml[%s]" % (PIP_CACHE_MOUNT, ",".join(features))
+            instruction.append(dockerfile)
+            if features is not None:
+                diff = set(features).difference(FEATURES)
+                if len(diff) > 0:
+                    raise InvalidArgument(
+                        f"Available features are: {FEATURES}. Invalid fields from provided: {diff}"
+                    )
+                PIP_CACHE_MOUNT = (
+                    "--mount=type=cache,target=/root/.cache/pip "
+                    if enable_buildkit
+                    else ""
+                )
+                instruction.append(
+                    "RUN %spip install bentoml[%s]"
+                    % (PIP_CACHE_MOUNT, ",".join(features))
+                )
+        else:
+            from _bentoml_impl.docker import generate_dockerfile
+
+            assert isinstance(options, BentoInfoV2)
+            dockerfile = generate_dockerfile(
+                options.image,
+                temp_fs,
+                enable_buildkit=enable_buildkit,
+                add_header=add_header,
+                envs=options.envs,
             )
+            instruction.append(dockerfile)
         temp_fs.writetext(dockerfile_path, "\n".join(instruction))
         yield tempdir, temp_fs.getsyspath(dockerfile_path)
 
diff --git a/src/bentoml/_internal/container/frontend/dockerfile/__init__.py b/src/bentoml/_internal/container/frontend/dockerfile/__init__.py
index 31d5a2c1..631c9465 100644
--- a/src/bentoml/_internal/container/frontend/dockerfile/__init__.py
+++ b/src/bentoml/_internal/container/frontend/dockerfile/__init__.py
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
 
 
 # Python supported versions
-SUPPORTED_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"]
+SUPPORTED_PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13"]
 # CUDA supported versions
 SUPPORTED_CUDA_VERSIONS = [
     "12.0.0",
@@ -72,9 +72,10 @@ CONTAINER_METADATA: dict[str, dict[str, t.Any]] = {
             "image": "amazonlinux:2",
             "supported_architectures": ["amd64", "arm64"],
         },
+        "install_command": "yum update && yum install -y {packages}",
     },
     "ubi8": {
-        "supported_python_versions": ["3.8", "3.9"],
+        "supported_python_versions": SUPPORTED_PYTHON_VERSIONS,
         "supported_cuda_versions": SUPPORTED_CUDA_VERSIONS,
         "python": {
             "image": "registry.access.redhat.com/ubi8/python-{spec_version}:1",
@@ -84,6 +85,7 @@ CONTAINER_METADATA: dict[str, dict[str, t.Any]] = {
             "image": "nvidia/cuda:{spec_version}-cudnn8-runtime-ubi8",
             "supported_architectures": ["amd64", "arm64", "ppc64le"],
         },
+        "install_command": "yum update && yum install -y {packages}",
     },
     "debian": {
         "supported_python_versions": SUPPORTED_PYTHON_VERSIONS,
@@ -100,6 +102,7 @@ CONTAINER_METADATA: dict[str, dict[str, t.Any]] = {
             "image": "continuumio/miniconda3:latest",
             "supported_architectures": SUPPORTED_ARCHITECTURES,
         },
+        "install_command": "apt-get update && apt-get install -y {packages}",
     },
     "alpine": {
         "supported_python_versions": SUPPORTED_PYTHON_VERSIONS,
@@ -112,6 +115,7 @@ CONTAINER_METADATA: dict[str, dict[str, t.Any]] = {
             "image": "continuumio/miniconda3:4.10.3p0-alpine",
             "supported_architectures": ["amd64"],
         },
+        "install_command": "apk update && apk add {packages}",
     },
 }
 
diff --git a/src/bentoml/_internal/container/frontend/dockerfile/entrypoint.sh b/src/bentoml/_internal/container/frontend/dockerfile/entrypoint.sh
index df1892dd..3ec4e7c7 100755
--- a/src/bentoml/_internal/container/frontend/dockerfile/entrypoint.sh
+++ b/src/bentoml/_internal/container/frontend/dockerfile/entrypoint.sh
@@ -10,6 +10,7 @@ _is_sourced() {
 }
 
 _main() {
+	[ -d .venv ] && source .venv/bin/activate
 	# For backwards compatibility with the yatai<1.0.0, adapting the old "yatai" command to the new "start" command.
 	if [ "${#}" -gt 0 ] && [ "${1}" = 'python' ] && [ "${2}" = '-m' ] && { [ "${3}" = 'bentoml._internal.server.cli.runner' ] || [ "${3}" = "bentoml._internal.server.cli.api_server" ]; }; then # SC2235, use { } to avoid subshell overhead
 		if [ "${3}" = 'bentoml._internal.server.cli.runner' ]; then
diff --git a/src/bentoml/_internal/container/frontend/dockerfile/templates/base_v2.j2 b/src/bentoml/_internal/container/frontend/dockerfile/templates/base_v2.j2
new file mode 100644
index 00000000..ec1936a5
--- /dev/null
+++ b/src/bentoml/_internal/container/frontend/dockerfile/templates/base_v2.j2
@@ -0,0 +1,97 @@
+{# BENTOML INTERNAL #}
+{# users can use these values #}
+{% import '_macros.j2' as common %}
+{% set bento__entrypoint = expands_bento_path('env', 'docker', 'entrypoint.sh', bento_path=bento__path) %}
+{% set __enable_buildkit__ = bento__enable_buildkit | default(False) -%}
+{% set __bento_envs__ = bento__envs | default([]) %}
+{% if __enable_buildkit__ %}
+# 1.2.1 is the current docker frontend that both buildkitd and kaniko supports.
+# syntax = {{ bento__buildkit_frontend }}
+#
+{% endif %}
+{% if bento__add_header %}
+# ===========================================
+#
+# THIS IS A GENERATED DOCKERFILE. DO NOT EDIT
+#
+# ===========================================
+{% endif %}
+
+# Block SETUP_BENTO_BASE_IMAGE
+{% block SETUP_BENTO_BASE_IMAGE %}
+FROM {{ __options__base_image }} AS base-container
+
+ENV LANG=C.UTF-8
+
+ENV LC_ALL=C.UTF-8
+
+ENV PYTHONIOENCODING=UTF-8
+
+ENV PYTHONUNBUFFERED=1
+{% endblock %}
+
+{% block SETUP_BENTO_USER %}
+ARG BENTO_USER={{ bento__user }}
+ARG BENTO_USER_UID={{ bento__uid_gid }}
+ARG BENTO_USER_GID={{ bento__uid_gid }}
+RUN groupadd -g $BENTO_USER_GID -o $BENTO_USER && useradd -m -u $BENTO_USER_UID -g $BENTO_USER_GID -o -r $BENTO_USER
+{% endblock %}
+
+{% block SETUP_BENTO_ENVARS %}
+
+ARG BENTO_PATH={{ bento__path }}
+ENV BENTO_PATH=$BENTO_PATH
+ENV BENTOML_HOME={{ bento__home }}
+ENV BENTOML_HF_CACHE_DIR={{ bento__path }}/hf-models
+
+{% for env in __bento_envs__ %}
+ARG {{ env.name }}{% if env.value %}={{ env.value }}{% endif %}
+
+ENV {{ env.name }}=${{ env.name }}
+{% endfor %}
+
+RUN mkdir $BENTO_PATH && chown {{ bento__user }}:{{ bento__user }} $BENTO_PATH -R
+WORKDIR $BENTO_PATH
+{% endblock %}
+
+{% block SETUP_BENTO_COMPONENTS %}
+{% set required_packages = "curl git" %}
+RUN command -v apt-get && apt-get update && apt-get -y install {{ required_packages }} \
+    || command -v apk && apk update && apk add {{ required_packages }} \
+    || yum update && yum install -y {{ required_packages }} \
+    || true
+
+{% for command in __options__commands %}
+RUN {{ command }}
+{% endfor %}
+
+RUN curl -LO https://astral.sh/uv/install.sh && \
+    sh install.sh && rm install.sh && mv $HOME/.cargo/bin/uv /usr/local/bin/ && uv venv --python {{ __options__python_version }}
+{% set __pip_cache__ = common.mount_cache("/root/.cache/") %}
+{% if __pip_preheat_packages__ %}
+{% for value in __pip_preheat_packages__ -%}
+{% call common.RUN(__enable_buildkit__) -%} {{ __pip_cache__ }} {% endcall -%} uv pip install {{ value|bash_quote }} ; exit 0
+{% endfor -%}
+{% endif -%}
+
+COPY --chown={{ bento__user }}:{{ bento__user }} ./env/python ./env/python/
+# install python packages
+{% call common.RUN(__enable_buildkit__) -%} {{ __pip_cache__ }} {% endcall -%} uv pip install -r ./env/python/requirements.txt
+COPY --chown={{ bento__user }}:{{ bento__user }} . ./
+{% endblock %}
+
+# Block SETUP_BENTO_ENTRYPOINT
+{% block SETUP_BENTO_ENTRYPOINT %}
+# Default port for BentoServer
+EXPOSE 3000
+
+# Expose Prometheus port
+EXPOSE {{ __prometheus_port__ }}
+
+RUN chmod +x {{ bento__entrypoint }}
+
+USER bentoml
+
+ENTRYPOINT [ "{{ bento__entrypoint }}" ]
+
+{% endblock %}
diff --git a/src/bentoml/_internal/container/generate.py b/src/bentoml/_internal/container/generate.py
index a25263c3..8282a2c1 100644
--- a/src/bentoml/_internal/container/generate.py
+++ b/src/bentoml/_internal/container/generate.py
@@ -32,6 +32,16 @@ BENTO_HOME = f"/home/{BENTO_USER}/"
 BENTO_PATH = f"{BENTO_HOME}bento"
 # 1.2.1 is the current docker frontend that both buildkitd and kaniko supports.
 BENTO_BUILDKIT_FRONTEND = "docker/dockerfile:1.2.1"
+PREHEAT_PIP_PACKAGES = ["torch", "vllm"]
+DEFAULT_BENTO_ENVS = {
+    "uid_gid": BENTO_UID_GID,
+    "user": BENTO_USER,
+    "home": BENTO_HOME,
+    "path": BENTO_PATH,
+    "add_header": True,
+    "buildkit_frontend": BENTO_BUILDKIT_FRONTEND,
+    "enable_buildkit": True,
+}
 
 
 def expands_bento_path(*path: str, bento_path: str = BENTO_PATH) -> str:
@@ -79,7 +89,7 @@ def get_templates_variables(
         spec = DistroSpec.from_options(docker, conda)
         python_version = docker.python_version
         assert docker.distro is not None and python_version is not None
-        if docker.distro in ("ubi8"):
+        if docker.distro in ("ubi8",):
             # ubi8 base images uses "py38" instead of "py3.8" in its image tag
             python_version = python_version.replace(".", "")
         base_image = spec.image.format(spec_version=python_version)
@@ -87,19 +97,7 @@ def get_templates_variables(
             base_image = spec.image.format(spec_version=docker.cuda_version)
 
     # bento__env
-    default_env = {
-        "uid_gid": BENTO_UID_GID,
-        "user": BENTO_USER,
-        "home": BENTO_HOME,
-        "path": BENTO_PATH,
-        "add_header": True,
-        "buildkit_frontend": BENTO_BUILDKIT_FRONTEND,
-        "enable_buildkit": True,
-    }
-    if bento_env:
-        default_env.update(bento_env)
-
-    PREHEAT_PIP_PACKAGES = ["torch", "vllm"]
+    default_env = {**DEFAULT_BENTO_ENVS, **bento_env}
 
     return {
         **{to_options_field(k): v for k, v in docker.to_dict().items()},
@@ -204,7 +202,7 @@ def generate_containerfile(
     if not os.path.exists(requirement_file):
         requirement_file = bento_fs.getsyspath("env/python/requirements.txt")
     if os.path.exists(requirement_file):
-        python_packages = _resolve_package_versions(requirement_file)
+        python_packages = resolve_package_versions(requirement_file)
     else:
         python_packages = {}
 
@@ -220,7 +218,7 @@ def generate_containerfile(
     )
 
 
-def _resolve_package_versions(requirement: str) -> dict[str, str]:
+def resolve_package_versions(requirement: str) -> dict[str, str]:
     from pip_requirements_parser import RequirementsFile
 
     requirements_txt = RequirementsFile.from_file(
diff --git a/src/bentoml/_internal/service/loader.py b/src/bentoml/_internal/service/loader.py
index 91d16611..7e8219de 100644
--- a/src/bentoml/_internal/service/loader.py
+++ b/src/bentoml/_internal/service/loader.py
@@ -17,7 +17,7 @@ from ...exceptions import NotFound
 from ..bento import Bento
 from ..bento.bento import BENTO_PROJECT_DIR_NAME
 from ..bento.bento import BENTO_YAML_FILENAME
-from ..bento.bento import DEFAULT_BENTO_BUILD_FILE
+from ..bento.bento import DEFAULT_BENTO_BUILD_FILES
 from ..bento.build_config import BentoBuildConfig
 from ..configuration import BENTOML_VERSION
 from ..configuration.containers import BentoMLContainer
@@ -386,33 +386,34 @@ def load(
                     f"Failed loading Bento from directory {bento_path}: {e}"
                 )
             logger.info("Service loaded from Bento directory: %s", svc)
-        elif os.path.isfile(
-            os.path.expanduser(os.path.join(bento_path, DEFAULT_BENTO_BUILD_FILE))
-        ):
-            # Loading from path to a project directory containing bentofile.yaml
+        else:
             try:
-                with open(
-                    os.path.join(bento_path, DEFAULT_BENTO_BUILD_FILE),
-                    "r",
-                    encoding="utf-8",
-                ) as f:
-                    build_config = BentoBuildConfig.from_yaml(f)
-                assert build_config.service, '"service" field in "bentofile.yaml" is required for loading the service, e.g. "service: my_service.py:svc"'
-                BentoMLContainer.model_aliases.set(build_config.model_aliases)
-                svc = import_service(
-                    build_config.service,
-                    working_dir=bento_path,
-                    standalone_load=standalone_load,
-                )
+                for filename in DEFAULT_BENTO_BUILD_FILES:
+                    if os.path.isfile(
+                        config_file := os.path.expanduser(
+                            os.path.join(bento_path, filename)
+                        )
+                    ):
+                        build_config = BentoBuildConfig.from_file(config_file)
+                        BentoMLContainer.model_aliases.set(build_config.model_aliases)
+                        svc = import_service(
+                            build_config.service,
+                            working_dir=bento_path,
+                            standalone_load=standalone_load,
+                        )
+                        logger.debug(
+                            "'%s' loaded from '%s': %s", svc.name, bento_path, svc
+                        )
+                        break
+                else:
+                    raise BentoMLException(
+                        f"Failed loading service from path {bento_path}. When loading from a path, it must be either a Bento "
+                        "containing bento.yaml or a project directory containing bentofile.yaml"
+                    )
             except ImportServiceError as e:
                 raise BentoMLException(
                     f"Failed loading Bento from directory {bento_path}: {e}"
                 )
-            logger.debug("'%s' loaded from '%s': %s", svc.name, bento_path, svc)
-        else:
-            raise BentoMLException(
-                f"Failed loading service from path {bento_path}. When loading from a path, it must be either a Bento containing bento.yaml or a project directory containing bentofile.yaml"
-            )
     else:
         try:
             # Loading from service definition file, e.g. "my_service.py:svc"
diff --git a/src/bentoml/_internal/utils/__init__.py b/src/bentoml/_internal/utils/__init__.py
index a45c8a8a..3c41fb15 100644
--- a/src/bentoml/_internal/utils/__init__.py
+++ b/src/bentoml/_internal/utils/__init__.py
@@ -606,7 +606,7 @@ def warn_deprecated(message: str, stacklevel: int = 2) -> None:
 
 def deprecated(
     name: str = "", deprecated_since: str = "1.4", suggestion: str = ""
-) -> t.Callable[[t.Callable[P, T]], t.Callable[P, T]]:
+) -> t.Callable[[C], C]:
     def decorator(func: t.Callable[P, T]) -> t.Callable[P, T]:
         obj_name = name or func.__name__
 
diff --git a/src/bentoml/_internal/utils/circus/watchfilesplugin.py b/src/bentoml/_internal/utils/circus/watchfilesplugin.py
index 6d065ddd..323a5dd3 100644
--- a/src/bentoml/_internal/utils/circus/watchfilesplugin.py
+++ b/src/bentoml/_internal/utils/circus/watchfilesplugin.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import logging
-import os
 import typing as t
 from pathlib import Path
 from threading import Event
@@ -80,16 +79,7 @@ class ServiceReloaderPlugin(CircusPlugin):
         )
 
     def create_spec(self) -> BentoPathSpec:
-        bentofile_path = os.path.join(self.working_dir, "bentofile.yaml")
-        if not os.path.exists(bentofile_path):
-            # if bentofile.yaml is not found, by default we will assume to watch all files
-            # via BentoBuildConfig.with_defaults()
-            build_config = BentoBuildConfig(service="").with_defaults()
-        else:
-            # respect bentofile.yaml include and exclude
-            with open(bentofile_path, "r") as f:
-                build_config = BentoBuildConfig.from_yaml(f).with_defaults()
-
+        build_config = BentoBuildConfig.from_bento_dir(self.working_dir)
         return BentoPathSpec(
             build_config.include, build_config.exclude, self.working_dir
         )
diff --git a/src/bentoml/bentos.py b/src/bentoml/bentos.py
index df9e1a00..0947c5b6 100644
--- a/src/bentoml/bentos.py
+++ b/src/bentoml/bentos.py
@@ -10,6 +10,8 @@ import typing as t
 from simple_di import Provide
 from simple_di import inject
 
+from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILES
+
 from ._internal.bento import Bento
 from ._internal.bento.build_config import BentoBuildConfig
 from ._internal.configuration.containers import BentoMLContainer
@@ -372,7 +374,7 @@ def build(
 
 @inject
 def build_bentofile(
-    bentofile: str = "bentofile.yaml",
+    bentofile: str | None = None,
     *,
     version: str | None = None,
     labels: dict[str, str] | None = None,
@@ -398,13 +400,22 @@ def build_bentofile(
     Returns:
         Bento: a Bento instance representing the materialized Bento saved in BentoStore
     """
-    try:
-        bentofile = resolve_user_filepath(bentofile, build_ctx)
-    except FileNotFoundError:
-        raise InvalidArgument(f'bentofile "{bentofile}" not found')
+    if bentofile:
+        try:
+            bentofile = resolve_user_filepath(bentofile, build_ctx)
+        except FileNotFoundError:
+            raise InvalidArgument(f'bentofile "{bentofile}" not found')
+    else:
+        for filename in DEFAULT_BENTO_BUILD_FILES:
+            try:
+                bentofile = resolve_user_filepath(filename, build_ctx)
+                break
+            except FileNotFoundError:
+                pass
+        else:
+            raise InvalidArgument("No bentofile found, please provide a bentofile path")
 
-    with open(bentofile, "r", encoding="utf-8") as f:
-        build_config = BentoBuildConfig.from_yaml(f)
+    build_config = BentoBuildConfig.from_file(bentofile)
 
     if labels:
         if not build_config.labels:
diff --git a/src/bentoml/images.py b/src/bentoml/images.py
new file mode 100644
index 00000000..82555463
--- /dev/null
+++ b/src/bentoml/images.py
@@ -0,0 +1,4 @@
+from _bentoml_sdk.images import Image
+from _bentoml_sdk.images import PythonImage
+
+__all__ = ["PythonImage", "Image"]
diff --git a/src/bentoml_cli/bentos.py b/src/bentoml_cli/bentos.py
index 307e7d70..c9f1f06d 100644
--- a/src/bentoml_cli/bentos.py
+++ b/src/bentoml_cli/bentos.py
@@ -82,7 +82,6 @@ def parse_delete_targets_argument_callback(
 def bento_management_commands() -> click.Group:
     import bentoml
     from bentoml import Tag
-    from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILE
     from bentoml._internal.configuration import get_quiet_mode
     from bentoml._internal.configuration.containers import BentoMLContainer
     from bentoml._internal.utils import human_readable_size
@@ -342,11 +341,7 @@ def bento_management_commands() -> click.Group:
     @bentos.command()
     @click.argument("build_ctx", type=click.Path(), default=".")
     @click.option(
-        "-f",
-        "--bentofile",
-        type=click.STRING,
-        default=DEFAULT_BENTO_BUILD_FILE,
-        help="Path to bentofile. Default to 'bentofile.yaml'",
+        "-f", "--bentofile", help="Path to bentofile. Default to 'bentofile.yaml'"
     )
     @click.option(
         "--version",
@@ -397,7 +392,7 @@ def bento_management_commands() -> click.Group:
     )
     def build(  # type: ignore (not accessed)
         build_ctx: str,
-        bentofile: str,
+        bentofile: str | None,
         version: str | None,
         labels: tuple[str, ...],
         output: t.Literal["tag", "default"],
diff --git a/src/bentoml_cli/containerize.py b/src/bentoml_cli/containerize.py
index ce8b21a4..ecdf3fca 100644
--- a/src/bentoml_cli/containerize.py
+++ b/src/bentoml_cli/containerize.py
@@ -143,7 +143,7 @@ def compatible_option(*param_decls: str, **attrs: t.Any):
         msg = attrs.pop("help", None)
         assert msg is not None, "'help' is required."
         attrs.setdefault("help", " ".join([prepend_msg, msg, append_msg]))
-        attrs.setdefault("expose_value", True)
+        attrs.setdefault("expose_value", False)
         attrs.setdefault("callback", obsolete_callback)
         return factory.option(*param_decls, **attrs)(f)
 
@@ -419,6 +419,7 @@ def buildx_options_group(f: F[t.Any]):
     multiple=True,
     callback=opt_callback,
     metavar="ARG=VALUE[,ARG=VALUE]",
+    expose_value=False,
 )
 @click.option(
     "--run-as-root",
@@ -442,7 +443,6 @@ def containerize_command(  # type: ignore
     enable_features: tuple[str] | None,
     run_as_root: bool,
     _memoized: dict[str, t.Any],
-    **kwargs: t.Any,  # pylint: disable=unused-argument
 ) -> None:
     """Containerizes given Bento into an OCI-compliant container, with any given OCI builder.
 
diff --git a/src/bentoml_cli/env_manager.py b/src/bentoml_cli/env_manager.py
index 3ce7ba8f..1110343d 100644
--- a/src/bentoml_cli/env_manager.py
+++ b/src/bentoml_cli/env_manager.py
@@ -14,7 +14,7 @@ from simple_di import Provide
 from simple_di import inject
 
 from bentoml._internal.bento.bento import BENTO_YAML_FILENAME
-from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILE
+from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILES
 from bentoml._internal.bento.bento import Bento
 from bentoml._internal.bento.bento import BentoStore
 from bentoml._internal.configuration import get_debug_mode
@@ -67,14 +67,17 @@ def get_environment(
             return EnvManager.from_bento(
                 env_type=env, bento=Bento.from_fs(bento_path_fs)
             ).environment
-        elif bento_path_fs.isfile(DEFAULT_BENTO_BUILD_FILE):
+        elif any(
+            bento_path_fs.isfile(filename) for filename in DEFAULT_BENTO_BUILD_FILES
+        ):
             # path to a bento project
             raise NotImplementedError(
                 "Serving from development project is not yet supported."
             )
         else:
             raise BentoMLException(
-                f"EnvManager failed to create an environment from path {bento_path_fs}. When loading from a path, it must be either a Bento or a project directory containing '{DEFAULT_BENTO_BUILD_FILE}'."
+                f"EnvManager failed to create an environment from path {bento_path_fs}. "
+                f"When loading from a path, it must be either a Bento or a project directory containing '{DEFAULT_BENTO_BUILD_FILES[0]}'."
             )
     else:
         try:
diff --git a/src/bentoml_cli/models.py b/src/bentoml_cli/models.py
index b0de315f..1d35fa88 100644
--- a/src/bentoml_cli/models.py
+++ b/src/bentoml_cli/models.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import json
+import os
 import typing as t
 
 import click
@@ -12,7 +13,7 @@ from simple_di import Provide
 from simple_di import inject
 
 from bentoml import Tag
-from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILE
+from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILES
 from bentoml._internal.bento.build_config import BentoBuildConfig
 from bentoml._internal.container import BentoMLContainer
 from bentoml._internal.utils import calc_dir_size
@@ -279,8 +280,6 @@ def import_from(model_path: str) -> None:  # type: ignore (not accessed)
 @click.option(
     "-F",
     "--bentofile",
-    type=click.STRING,
-    default=DEFAULT_BENTO_BUILD_FILE,
     help="Path to bentofile. Default to 'bentofile.yaml'",
 )
 @click.pass_context
@@ -289,7 +288,7 @@ def pull(
     ctx: click.Context,
     model_tag: str | None,
     force: bool,
-    bentofile: str,
+    bentofile: str | None,
     cloud_client: BentoCloudClient = Provide[BentoMLContainer.bentocloud_client],
 ):
     """Pull Model from a remote model store. If model_tag is not provided,
@@ -303,13 +302,20 @@ def pull(
         cloud_client.model.pull(model_tag, force=force)
         return
 
-    try:
-        bentofile = resolve_user_filepath(bentofile, None)
-    except FileNotFoundError:
-        raise InvalidArgument(f'bentofile "{bentofile}" not found')
-
-    with open(bentofile, "r", encoding="utf-8") as f:
-        build_config = BentoBuildConfig.from_yaml(f)
+    if bentofile:
+        try:
+            bentofile = resolve_user_filepath(bentofile, None)
+        except FileNotFoundError:
+            raise InvalidArgument(f'file "{bentofile}" not found')
+    else:
+        for filename in DEFAULT_BENTO_BUILD_FILES:
+            if os.path.exists(filename):
+                build_config = BentoBuildConfig.from_file(filename)
+                break
+        else:
+            raise InvalidArgument(
+                "No bentofile.yaml or pyproject.toml found, please provide a bentofile path"
+            )
 
     if not build_config.models:
         raise InvalidArgument(


Test patch:
diff --git a/tests/unit/_internal/bento/test_bento.py b/tests/unit/_internal/bento/test_bento.py
index c3374de2..26834509 100644
--- a/tests/unit/_internal/bento/test_bento.py
+++ b/tests/unit/_internal/bento/test_bento.py
@@ -13,6 +13,7 @@ import pytest
 from bentoml import Tag
 from bentoml import bentos
 from bentoml._internal.bento import Bento
+from bentoml._internal.bento.bento import BaseBentoInfo
 from bentoml._internal.bento.bento import BentoApiInfo
 from bentoml._internal.bento.bento import BentoInfo
 from bentoml._internal.bento.bento import BentoModelInfo
@@ -93,6 +94,11 @@ models:
   module: model_b_module
   creation_time: '{model_creation_time}'
   alias: model_b_alias
+entry_service: ''
+services: []
+envs: []
+schema: {{}}
+spec: 1
 runners:
 - name: runner_a
   runnable_type: test_runnable_a
@@ -101,10 +107,6 @@ runners:
   - runner_a_model
   resource_config:
     cpu: 2
-entry_service: ''
-services: []
-envs: []
-schema: {{}}
 apis:
 - name: predict
   input_type: NumpyNdarray
@@ -146,7 +148,7 @@ conda:
         )
 
     with open(bento_yaml_b_filename, encoding="utf-8") as bento_yaml_b:
-        bentoinfo_b_from_yaml = BentoInfo.from_yaml_file(bento_yaml_b)
+        bentoinfo_b_from_yaml = BaseBentoInfo.from_yaml_file(bento_yaml_b)
 
         assert bentoinfo_b_from_yaml == bentoinfo_b
 
diff --git a/tests/unit/_internal/service_loader/test_service_loader.py b/tests/unit/_internal/service_loader/test_service_loader.py
index 2ac50795..85055082 100644
--- a/tests/unit/_internal/service_loader/test_service_loader.py
+++ b/tests/unit/_internal/service_loader/test_service_loader.py
@@ -19,8 +19,7 @@ def test_load_service_from_directory():
 @pytest.mark.usefixtures("change_test_dir", "model_store")
 def test_load_service_from_bento():
     sys.modules.pop("service", None)
-    with open("./bento_with_models/bentofile.yaml") as f:
-        build_config = BentoBuildConfig.from_yaml(f)
+    build_config = BentoBuildConfig.from_file("./bento_with_models/bentofile.yaml")
     bento = Bento.create(
         build_config=build_config, version="1.0", build_ctx="./bento_with_models"
     ).save()