From a0fc148a1e59a80a5566c68f97f0fa2667e56a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Celal=20Zamano=C4=9Flu?= Date: Thu, 4 Jun 2026 09:37:04 +0300 Subject: [PATCH] fix(SKY-9781): port prebuilt-UI Dockerfile + entrypoint from cloud (#5998) Co-authored-by: Claude Opus 4.7 (1M context) --- Dockerfile.ui | 55 ++++++++++++++++----- Dockerfile.ui.dockerignore | 15 ++++++ entrypoint-skyvernui.sh | 98 +++++++++++++++++++++++++++++++------- 3 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 Dockerfile.ui.dockerignore diff --git a/Dockerfile.ui b/Dockerfile.ui index 8005922dd..dd146f663 100644 --- a/Dockerfile.ui +++ b/Dockerfile.ui @@ -1,28 +1,61 @@ -FROM node:20.12-slim - -# Install tini for proper signal handling and zombie reaping -RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* +# Stage 1: Build frontend assets +FROM node:24.14-slim AS builder WORKDIR /app -COPY ./skyvern-frontend /app -COPY ./entrypoint-skyvernui.sh /app/entrypoint-skyvernui.sh -RUN npm install -# Placeholders for runtime injection (will be replaced by entrypoint script) +# Copy dependency files first for better Docker layer caching +COPY ./skyvern-frontend/package.json ./skyvern-frontend/package-lock.json ./ +RUN npm ci + +# Copy source code +COPY ./skyvern-frontend/ ./ + +# Placeholders for runtime injection (replaced by entrypoint script at container start). +# Every VITE_* variable that the frontend reads via import.meta.env must have a +# placeholder here so Vite bakes the placeholder string into the JS bundle, +# allowing sed replacement at container start. ENV VITE_API_BASE_URL=__VITE_API_BASE_URL_PLACEHOLDER__ ENV VITE_WSS_BASE_URL=__VITE_WSS_BASE_URL_PLACEHOLDER__ ENV VITE_ARTIFACT_API_BASE_URL=__VITE_ARTIFACT_API_BASE_URL_PLACEHOLDER__ ENV VITE_SKYVERN_API_KEY=__SKYVERN_API_KEY_PLACEHOLDER__ ENV VITE_BROWSER_STREAMING_MODE=__VITE_BROWSER_STREAMING_MODE_PLACEHOLDER__ +ENV VITE_ENABLE_LOG_ARTIFACTS=__VITE_ENABLE_LOG_ARTIFACTS_PLACEHOLDER__ +ENV VITE_ENABLE_CODE_BLOCK=__VITE_ENABLE_CODE_BLOCK_PLACEHOLDER__ +ENV VITE_ENABLE_2FA_NOTIFICATIONS=__VITE_ENABLE_2FA_NOTIFICATIONS_PLACEHOLDER__ # APP_VERSION is baked into the JS bundle at build time via Vite's define. # Pass --build-arg APP_VERSION=$(git rev-parse HEAD) when building the image. ARG APP_VERSION=development ENV APP_VERSION=$APP_VERSION -# Build at image time +# Vite's rollup phase needs more than V8's default ~2GB heap. +ENV NODE_OPTIONS=--max-old-space-size=4096 + +# Build assets at image build time (with placeholder values baked in) RUN npm run build -# Use tini as init for proper signal handling and zombie reaping +# Stage 2: Runtime image (only built assets + servers) +FROM node:24.14-slim + +# Install tini for proper signal handling and zombie reaping +RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy built assets and server files from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/localServer.js ./ +COPY --from=builder /app/artifactServer.js ./ +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/package-lock.json ./ + +# Install only production dependencies needed for the servers +RUN npm ci --omit=dev + +COPY ./entrypoint-skyvernui.sh ./entrypoint-skyvernui.sh +RUN chmod +x ./entrypoint-skyvernui.sh + +EXPOSE 8080 9090 + ENTRYPOINT ["/usr/bin/tini", "--"] -CMD ["/bin/bash", "/app/entrypoint-skyvernui.sh"] +CMD ["/bin/bash", "./entrypoint-skyvernui.sh"] diff --git a/Dockerfile.ui.dockerignore b/Dockerfile.ui.dockerignore new file mode 100644 index 000000000..039e07be4 --- /dev/null +++ b/Dockerfile.ui.dockerignore @@ -0,0 +1,15 @@ +# Per-Dockerfile ignore for Dockerfile.ui (overrides .dockerignore). +# The UI image only needs the frontend source and the entrypoint script; +# everything else in the repo is irrelevant to this build context. + +* + +!skyvern-frontend +!entrypoint-skyvernui.sh + +# Re-exclude noise inside skyvern-frontend so host-side state (a populated +# node_modules whose .bin shims don't resolve inside Linux, a stale dist/, +# local .env files) can't leak into the build. +skyvern-frontend/node_modules +skyvern-frontend/dist +skyvern-frontend/.env* diff --git a/entrypoint-skyvernui.sh b/entrypoint-skyvernui.sh index eea748ac6..a22940d11 100644 --- a/entrypoint-skyvernui.sh +++ b/entrypoint-skyvernui.sh @@ -1,21 +1,45 @@ #!/bin/bash -set -e +set -euo pipefail + +# Escape special sed characters in a value so it can be used safely in a +# sed replacement string (s|old|new|g). Handles |, &, \, and / which are +# metacharacters. Only handles single-line values (sufficient for URLs, +# API keys, booleans, and path prefixes used here); a multi-line value +# would corrupt the bundle silently, so fail fast instead. +escape_sed() { + if [[ "$1" == *$'\n'* ]]; then + echo "ERROR: escape_sed called with multi-line value; refusing to corrupt the bundle" >&2 + exit 1 + fi + printf '%s\n' "$1" | sed -e 's/[&/\|]/\\&/g' +} # Default values for environment variables VITE_API_BASE_URL="${VITE_API_BASE_URL:-http://localhost:8000/api/v1}" VITE_WSS_BASE_URL="${VITE_WSS_BASE_URL:-ws://localhost:8000/api/v1}" VITE_ARTIFACT_API_BASE_URL="${VITE_ARTIFACT_API_BASE_URL:-http://localhost:9090}" VITE_BROWSER_STREAMING_MODE="${VITE_BROWSER_STREAMING_MODE:-vnc}" +VITE_ENABLE_LOG_ARTIFACTS="${VITE_ENABLE_LOG_ARTIFACTS:-false}" +VITE_ENABLE_CODE_BLOCK="${VITE_ENABLE_CODE_BLOCK:-false}" +VITE_ENABLE_2FA_NOTIFICATIONS="${VITE_ENABLE_2FA_NOTIFICATIONS:-false}" # Priority for VITE_SKYVERN_API_KEY: # 1. Environment variable (from .env file or docker-compose environment), -# but only if it looks like a real key (not a placeholder) +# but only if it's not a placeholder/sentinel value. Both the Dockerfile +# default (__SKYVERN_API_KEY_PLACEHOLDER__) and the .env.example default +# (YOUR_API_KEY) can leak through if users skip configuration. Keep this +# filter list aligned with the frontend's PLACEHOLDER_VALUES. # 2. Generated credentials file from the backend first-run setup +# (volume-mounted at /app/.skyvern/credentials.toml in docker-compose) +VITE_SKYVERN_API_KEY="${VITE_SKYVERN_API_KEY:-}" SKYVERN_CREDENTIALS_FILE="${SKYVERN_CREDENTIALS_FILE:-/app/.skyvern/credentials.toml}" GENERATED_KEY=$(sed -n 's/.*cred\s*=\s*"\([^"]*\)".*/\1/p' "$SKYVERN_CREDENTIALS_FILE" 2>/dev/null || echo "") -if [ -n "$VITE_SKYVERN_API_KEY" ] && [ "$VITE_SKYVERN_API_KEY" != "YOUR_API_KEY" ]; then - VITE_SKYVERN_API_KEY=$(echo "$VITE_SKYVERN_API_KEY" | xargs) +if [ -n "$VITE_SKYVERN_API_KEY" ] \ + && [ "$VITE_SKYVERN_API_KEY" != "__SKYVERN_API_KEY_PLACEHOLDER__" ] \ + && [ "$VITE_SKYVERN_API_KEY" != "YOUR_API_KEY" ]; then + VITE_SKYVERN_API_KEY="${VITE_SKYVERN_API_KEY#"${VITE_SKYVERN_API_KEY%%[![:space:]]*}"}" + VITE_SKYVERN_API_KEY="${VITE_SKYVERN_API_KEY%"${VITE_SKYVERN_API_KEY##*[![:space:]]}"}" echo "Using VITE_SKYVERN_API_KEY from environment variable" elif [ -n "$GENERATED_KEY" ]; then VITE_SKYVERN_API_KEY="$GENERATED_KEY" @@ -25,18 +49,60 @@ else VITE_SKYVERN_API_KEY="" fi -# Inject environment variables into pre-built JS files (replace placeholders) -# Using | as delimiter since URLs contain / -find /app/dist -name "*.js" -exec sed -i \ - -e "s|__VITE_API_BASE_URL_PLACEHOLDER__|${VITE_API_BASE_URL}|g" \ - -e "s|__VITE_WSS_BASE_URL_PLACEHOLDER__|${VITE_WSS_BASE_URL}|g" \ - -e "s|__VITE_ARTIFACT_API_BASE_URL_PLACEHOLDER__|${VITE_ARTIFACT_API_BASE_URL}|g" \ - -e "s|__SKYVERN_API_KEY_PLACEHOLDER__|${VITE_SKYVERN_API_KEY}|g" \ - -e "s|__VITE_BROWSER_STREAMING_MODE_PLACEHOLDER__|${VITE_BROWSER_STREAMING_MODE}|g" \ - {} \; +echo "Injecting runtime environment variables into pre-built assets..." -# Start the servers (no rebuild needed) -# Tini (configured as ENTRYPOINT) handles signal forwarding and zombie reaping +# NOTE: Placeholder replacement is one-shot (sed -i modifies files in place). +# If environment variables change, the container must be recreated (not just +# restarted) to apply new values. + +# Escape all values for safe use in sed replacement patterns +ESC_API_BASE_URL=$(escape_sed "$VITE_API_BASE_URL") +ESC_WSS_BASE_URL=$(escape_sed "$VITE_WSS_BASE_URL") +ESC_ARTIFACT_API_BASE_URL=$(escape_sed "$VITE_ARTIFACT_API_BASE_URL") +ESC_SKYVERN_API_KEY=$(escape_sed "$VITE_SKYVERN_API_KEY") +ESC_BROWSER_STREAMING_MODE=$(escape_sed "$VITE_BROWSER_STREAMING_MODE") +ESC_ENABLE_LOG_ARTIFACTS=$(escape_sed "$VITE_ENABLE_LOG_ARTIFACTS") +ESC_ENABLE_CODE_BLOCK=$(escape_sed "$VITE_ENABLE_CODE_BLOCK") +ESC_ENABLE_2FA_NOTIFICATIONS=$(escape_sed "$VITE_ENABLE_2FA_NOTIFICATIONS") + +# Replace placeholder strings in pre-built JS and HTML files with actual runtime values +find /app/dist \( -name "*.js" -o -name "*.html" \) -exec sed -i \ + -e "s|__VITE_API_BASE_URL_PLACEHOLDER__|${ESC_API_BASE_URL}|g" \ + -e "s|__VITE_WSS_BASE_URL_PLACEHOLDER__|${ESC_WSS_BASE_URL}|g" \ + -e "s|__VITE_ARTIFACT_API_BASE_URL_PLACEHOLDER__|${ESC_ARTIFACT_API_BASE_URL}|g" \ + -e "s|__SKYVERN_API_KEY_PLACEHOLDER__|${ESC_SKYVERN_API_KEY}|g" \ + -e "s|__VITE_BROWSER_STREAMING_MODE_PLACEHOLDER__|${ESC_BROWSER_STREAMING_MODE}|g" \ + -e "s|__VITE_ENABLE_LOG_ARTIFACTS_PLACEHOLDER__|${ESC_ENABLE_LOG_ARTIFACTS}|g" \ + -e "s|__VITE_ENABLE_CODE_BLOCK_PLACEHOLDER__|${ESC_ENABLE_CODE_BLOCK}|g" \ + -e "s|__VITE_ENABLE_2FA_NOTIFICATIONS_PLACEHOLDER__|${ESC_ENABLE_2FA_NOTIFICATIONS}|g" \ + {} + + +# Sanity check: ensure ALL placeholders were actually replaced. +# If Vite ever splits/mangles a placeholder string, sed will silently +# succeed while leaving the placeholder in the bundle — catch that here. +# Scoped to *.js and *.html (the files sed modifies above) so unmodified +# sourcemaps don't trigger false positives. +if grep -rqE --include='*.js' --include='*.html' \ + '__VITE_[A-Z0-9_]+_PLACEHOLDER__|__SKYVERN_API_KEY_PLACEHOLDER__' /app/dist/; then + echo "ERROR: Placeholder replacement incomplete in dist/. Unreplaced placeholders:" >&2 + grep -rhoE --include='*.js' --include='*.html' \ + '__VITE_[A-Z0-9_]+_PLACEHOLDER__|__SKYVERN_API_KEY_PLACEHOLDER__' /app/dist/ \ + | sort -u | sed 's/^/ /' >&2 + exit 1 +fi + +echo "Starting servers..." + +# Ensure both servers are cleaned up if either one exits unexpectedly +trap 'kill $(jobs -p) 2>/dev/null; wait' EXIT + +# Start the local server (serves pre-built static assets) and artifact server node localServer.js & node artifactServer.js & -wait + +# Exit as soon as either server terminates so the container restarts promptly. +# `wait -n` requires bash 4.3+; the base image (node:24.14-slim / Debian 12) +# ships bash 5.2 — revisit if the base image is ever swapped for one with an +# older bash (e.g. alpine's ash, which doesn't support `wait -n` at all). +wait -n +exit $?