Source code for agentscope_runtime.engine.deployers.utils.docker_image_utils.dockerfile_generator
# -*- coding: utf-8 -*-
import logging
import os
import tempfile
from typing import Optional, Dict, List
from pydantic import BaseModel
logger = logging.getLogger(__name__)
[docs]
class DockerfileConfig(BaseModel):
"""Configuration for Dockerfile generation"""
base_image: str = "python:3.10-slim-bookworm"
port: int = 8000
working_dir: str = "/app"
user: str = "appuser"
additional_packages: List[str] = []
env_vars: Dict[str, str] = {}
startup_command: Optional[str] = None
health_check_endpoint: str = "/health"
custom_template: Optional[str] = None
platform: Optional[str] = None
[docs]
class DockerfileGenerator:
"""
Responsible for generating Dockerfiles from templates.
Separated from image building for better modularity.
"""
# Default Dockerfile template for Python applications
DEFAULT_TEMPLATE = """# Use official Python runtime as base image
FROM --platform={platform} {base_image}
# Set working directory in container
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Configure package sources for better performance
RUN rm -f /etc/apt/sources.list.d/*.list
# add aliyun mirrors
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib " \
"non-free non-free-firmware" > /etc/apt/sources.list && \
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main " \
"contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
echo "deb https://mirrors.aliyun.com/debian-security/ " \
"bookworm-security main contrib non-free " \
"non-free-firmware" >> /etc/apt/sources.list
# replace debian to aliyun
RUN mkdir -p /etc/apt/sources.list.d && \
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
EOF
# Clean up package lists
RUN rm -rf /var/lib/apt/lists/*
# Install system dependencies
RUN apt-get update && apt-get install -y \\
gcc \\
curl \\
{additional_packages_section} && rm -rf /var/lib/apt/lists/*
# Copy project files
COPY . {working_dir}/
# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip
RUN if [ -f requirements.txt ]; then \\
pip install --no-cache-dir -r requirements.txt \\
-i https://pypi.tuna.tsinghua.edu.cn/simple; fi
# Create non-root user for security
RUN adduser --disabled-password --gecos '' {user} && \\
chown -R {user} {working_dir}
USER {user}
{env_vars_section}
# Expose port
EXPOSE {port}
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
CMD curl -f http://localhost:{port}{health_check_endpoint} || exit 1
# Command to run the application
{startup_command_section}"""
[docs]
def __init__(self) -> None:
self.temp_files: List[str] = []
[docs]
def generate_dockerfile_content(self, config: DockerfileConfig) -> str:
"""
Generate Dockerfile content from configuration.
Args:
config: Dockerfile configuration
Returns:
str: Generated Dockerfile content
"""
template = config.custom_template or self.DEFAULT_TEMPLATE
# Prepare additional packages section
additional_packages_section = ""
if config.additional_packages:
packages_line = " \\\n ".join(config.additional_packages)
additional_packages_section = f" {packages_line} \\\n"
# Prepare environment variables section
env_vars_section = ""
if config.env_vars:
env_vars_section = "\n# Additional environment variables\n"
for key, value in config.env_vars.items():
env_vars_section += f"ENV {key}={value}\n"
env_vars_section += "\n"
# Prepare startup command section
if config.startup_command:
if config.startup_command.startswith("["):
# JSON array format
startup_command_section = f"CMD {config.startup_command}"
else:
# Shell format
startup_command_section = f'CMD ["{config.startup_command}"]'
else:
# Default uvicorn command
startup_command_section = (
f'CMD ["uvicorn", "main:app", "--host", "0.0.0.0", '
f'"--port", "{config.port}"]'
)
# Format template with configuration values
content = template.format(
base_image=config.base_image,
working_dir=config.working_dir,
port=config.port,
user=config.user,
health_check_endpoint=config.health_check_endpoint,
additional_packages_section=additional_packages_section,
env_vars_section=env_vars_section,
startup_command_section=startup_command_section,
platform=config.platform,
)
return content
[docs]
def create_dockerfile(
self,
config: DockerfileConfig,
output_dir: Optional[str] = None,
) -> str:
"""
Create Dockerfile in specified directory.
Args:
config: Dockerfile configuration
output_dir: Directory to create Dockerfile (temp dir if None)
Returns:
str: Path to created Dockerfile
"""
# Create output directory if not provided
if output_dir is None:
output_dir = tempfile.mkdtemp(prefix="dockerfile_")
self.temp_files.append(output_dir)
else:
os.makedirs(output_dir, exist_ok=True)
# Generate Dockerfile content
dockerfile_content = self.generate_dockerfile_content(config)
# Write Dockerfile
dockerfile_path = os.path.join(output_dir, "Dockerfile")
try:
with open(dockerfile_path, "w", encoding="utf-8") as f:
f.write(dockerfile_content)
logger.info(f"Created Dockerfile: {dockerfile_path}")
return dockerfile_path
except Exception as e:
logger.error(f"Failed to create Dockerfile: {e}")
if output_dir in self.temp_files and os.path.exists(output_dir):
import shutil
shutil.rmtree(output_dir)
self.temp_files.remove(output_dir)
raise
[docs]
def validate_config(self, config: DockerfileConfig) -> bool:
"""
Validate Dockerfile configuration.
Args:
config: Configuration to validate
Returns:
bool: True if valid
Raises:
ValueError: If configuration is invalid
"""
if not config.base_image:
raise ValueError("Base image cannot be empty")
if (
not isinstance(config.port, int)
or config.port <= 0
or config.port > 65535
):
raise ValueError(f"Invalid port: {config.port}")
if not config.working_dir.startswith("/"):
raise ValueError(
f"Working directory must be absolute path: "
f"{config.working_dir}",
)
return True
[docs]
def cleanup(self):
"""Clean up temporary files"""
import shutil
for temp_path in self.temp_files:
if os.path.exists(temp_path):
try:
shutil.rmtree(temp_path)
logger.debug(f"Cleaned up temp path: {temp_path}")
except OSError as e:
logger.warning(f"Failed to cleanup {temp_path}: {e}")
self.temp_files.clear()
[docs]
def __enter__(self):
"""Context manager entry"""
return self
[docs]
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit with cleanup"""
self.cleanup()