fix(SKY-9781): port prebuilt-UI Dockerfile + entrypoint from cloud (#5998)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Celal Zamanoğlu 2026-06-04 09:37:04 +03:00 committed by GitHub
parent 07da906667
commit a0fc148a1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 141 additions and 27 deletions

View file

@ -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"]

View file

@ -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*

View file

@ -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 $?