File size: 5,441 Bytes
e462aae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src

# Copy solution and project files for better layer caching
COPY ["RoslynStone.sln", "./"]
COPY ["src/RoslynStone.Api/RoslynStone.Api.csproj", "src/RoslynStone.Api/"]
COPY ["src/RoslynStone.Core/RoslynStone.Core.csproj", "src/RoslynStone.Core/"]
COPY ["src/RoslynStone.Infrastructure/RoslynStone.Infrastructure.csproj", "src/RoslynStone.Infrastructure/"]
COPY ["src/RoslynStone.ServiceDefaults/RoslynStone.ServiceDefaults.csproj", "src/RoslynStone.ServiceDefaults/"]
COPY ["src/RoslynStone.GradioModule/RoslynStone.GradioModule.csproj", "src/RoslynStone.GradioModule/"]

# Restore dependencies (this layer will be cached if project files don't change)
RUN dotnet restore "src/RoslynStone.Api/RoslynStone.Api.csproj"

# Copy all source files
COPY . .

# Build the application
WORKDIR "/src/src/RoslynStone.Api"
# Build into the standard bin/$(Configuration)/$(TargetFramework) output which publish expects
RUN dotnet build "RoslynStone.Api.csproj" \
    -c $BUILD_CONFIGURATION \
    --no-restore

# Publish stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release

# Install CSnakes.Stage tool for Python environment setup
RUN dotnet tool install --global CSnakes.Stage
ENV PATH="/root/.dotnet/tools:${PATH}"

# Set up Python environment with CSnakes
# This downloads Python 3.12 redistributable and creates a venv
RUN setup-python --python 3.12 --venv /app/.venv --verbose

# Install UV (fast Python package installer) - only needed in build stage
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"

# Install all Python dependencies using UV into the venv
# This happens at build time, not runtime, for faster container startup
# --prerelease=allow is needed because gradio 6.0.0 depends on gradio-client 2.0.0.dev3
# We install dependencies directly rather than using editable install since this is just a script, not a package
WORKDIR "/src/src/RoslynStone.GradioModule"
RUN uv pip install \
    "gradio>=6.0.0" \
    "httpx>=0.27.0" \
    "pygments>=2.17.0" \
    "openai>=1.0.0" \
    "anthropic>=0.25.0" \
    "google-generativeai>=0.3.0" \
    "huggingface_hub>=0.20.0" \
    --python /app/.venv/bin/python \
    --prerelease=allow

# Verify Gradio is installed correctly
RUN /app/.venv/bin/python3 -c "import gradio; print(f'Gradio {gradio.__version__} installed successfully')"

# Publish the application with ReadyToRun (R2R) for faster startup
# Note: We cannot use Native AOT because Roslyn requires dynamic code compilation
# R2R provides a hybrid approach - pre-compiled code with JIT fallback for dynamic scenarios
WORKDIR "/src/src/RoslynStone.Api"
RUN dotnet publish "RoslynStone.Api.csproj" \
    -c $BUILD_CONFIGURATION \
    -o /app/publish \
    --no-restore \
    --no-build \
    /p:UseAppHost=false \
    /p:PublishReadyToRun=true \
    /p:PublishSingleFile=false \
    /p:PublishTrimmed=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app

# Create non-root user for security
RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser

# Copy published application
COPY --from=publish /app/publish .
COPY src/RoslynStone.Api/entrypoint.sh .
RUN chmod +x entrypoint.sh

# Copy CSnakes Python redistributable from build stage
COPY --from=publish /root/.config/CSnakes /home/appuser/.config/CSnakes

# Some venv scripts created during the publish stage contain shebangs
# that reference the root user's CSnakes path (/root/.config/CSnakes/...).
# Create a root-side symlink to the copied location so those shebangs
# keep working in the runtime image.
RUN mkdir -p /root/.config \
    && ln -s /home/appuser/.config/CSnakes /root/.config/CSnakes || true

# Copy Python virtual environment with pre-installed Gradio from build stage
COPY --from=publish /app/.venv .venv

# Verify venv was copied correctly
RUN test -f /app/.venv/bin/python3 && test -f /app/.venv/bin/pip || \
    (echo "ERROR: Virtual environment not copied correctly!" && exit 1)

# Create symlink for backward compatibility (some code may reference /app/venv)
RUN ln -s /app/.venv /app/venv
# Change ownership to non-root user
RUN chown -R appuser:appuser /app /home/appuser/.config

# Set environment variables for MCP server and Python
# MCP_TRANSPORT: "stdio" (default) or "http"
# When using HTTP transport, set ASPNETCORE_URLS to configure listening address
# DOTNET_RUNNING_IN_CONTAINER tells the app to skip Python dependency installation
# since dependencies are pre-installed during docker build
ENV DOTNET_ENVIRONMENT=Production \
    MCP_TRANSPORT=stdio \
    ASPNETCORE_URLS= \
    DOTNET_EnableDiagnostics=0 \
    DOTNET_RUNNING_IN_CONTAINER=true \
    LD_LIBRARY_PATH=/home/appuser/.config/CSnakes/python3.12.9/python/install/lib \
    PYTHONHOME=/home/appuser/.config/CSnakes/python3.12.9/python/install

# Switch to non-root user
USER appuser

# The MCP server supports both stdio and HTTP transports
# - Stdio (default): Set MCP_TRANSPORT=stdio, no ports exposed
# - HTTP: Set MCP_TRANSPORT=http and ASPNETCORE_URLS=http://+:8080, then EXPOSE 8080
#   In HTTP mode, Gradio landing page will be available at root (/)
# Telemetry will be sent to the OTEL endpoint configured via environment variables

# Example for HTTP mode:
# ENV MCP_TRANSPORT=http
# ENV ASPNETCORE_URLS=http://+:8080
# EXPOSE 8080

ENTRYPOINT ["./entrypoint.sh"]