diff --git a/api/Dockerfile b/api/Dockerfile index e7dbcc4e..ee8c44fb 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -209,6 +209,12 @@ ENV PORT=2000 \ EXPOSE 2000/tcp ENTRYPOINT ["/sandbox_api/entrypoint.sh"] +# Direct NsJail runtime with packages baked in (no external mount needed). +# Combines local-oci-runtime env with package-builder output. +FROM local-oci-runtime AS sandbox-nsjail + +COPY --from=package-builder /pkgs /pkgs + # Dedicated resident-process host. This image deliberately keeps the runner # control listener on 8080 and exposes one fixed user-app port separately. # `/execute` is disabled when hosted-app mode is enabled; the control plane diff --git a/docker-compose.yaml b/docker-compose.yaml index a4a43cdd..90720d2b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -53,7 +53,7 @@ services: - CODEAPI_INTERNAL_SERVICE_TOKEN=${CODEAPI_INTERNAL_SERVICE_TOKEN:-localdev-internal-service-token} - REDIS_HOST=redis - REDIS_PORT=6379 - - REDIS_PASSWORD=localdev + - REDIS_PASSWORD=${REDIS_PASSWORD:-localdev} depends_on: - redis - tool_call_server @@ -92,7 +92,7 @@ services: - CODEAPI_INTERNAL_SERVICE_TOKEN=${CODEAPI_INTERNAL_SERVICE_TOKEN:-localdev-internal-service-token} - REDIS_HOST=redis - REDIS_PORT=6379 - - REDIS_PASSWORD=localdev + - REDIS_PASSWORD=${REDIS_PASSWORD:-localdev} - PYTHON_CONCURRENCY=${PYTHON_CONCURRENCY:-1} - OTHER_CONCURRENCY=${OTHER_CONCURRENCY:-8} - WORKER_HEALTH_PORT=3113 @@ -118,11 +118,15 @@ services: - EGRESS_GATEWAY_FILE_SERVER_URL=http://file_server:3000 - EGRESS_GATEWAY_TOOL_CALL_SERVER_URL=http://tool_call_server:3033 - EGRESS_GATEWAY_MAX_TOOL_CALL_BYTES=1048576 - - CODEAPI_EGRESS_GRANT_SECRET=${CODEAPI_EGRESS_GRANT_SECRET:-localdev-egress-grant-secret-change-me-32b} + # Scoped to the gateway only. Coolify injects app-level compose + # variables into every service, and CODEAPI_EGRESS_GRANT_SECRET is + # rejected on api/service-worker startup in hardened mode — so the + # overridable app var uses the gateway-specific name. + - CODEAPI_EGRESS_GRANT_SECRET=${CODEAPI_GATEWAY_EGRESS_GRANT_SECRET:-localdev-egress-grant-secret-change-me-32b} - CODEAPI_INTERNAL_SERVICE_TOKEN=${CODEAPI_INTERNAL_SERVICE_TOKEN:-localdev-internal-service-token} - REDIS_HOST=redis - REDIS_PORT=6379 - - REDIS_PASSWORD=localdev + - REDIS_PASSWORD=${REDIS_PASSWORD:-localdev} depends_on: - file_server - tool_call_server @@ -140,7 +144,7 @@ services: - TOOL_CALL_SESSION_EXPIRY=600 - REDIS_HOST=redis - REDIS_PORT=6379 - - REDIS_PASSWORD=localdev + - REDIS_PASSWORD=${REDIS_PASSWORD:-localdev} - CODEAPI_INTERNAL_SERVICE_TOKEN=${CODEAPI_INTERNAL_SERVICE_TOKEN:-localdev-internal-service-token} depends_on: - redis @@ -149,7 +153,7 @@ services: build: context: . dockerfile: api/Dockerfile - target: sandbox-runner-${KVM_ENABLED:-true} + target: sandbox-runner-true container_name: sandbox-runner restart: always privileged: false @@ -160,11 +164,14 @@ services: volumes: - ${SANDBOX_PACKAGES_PATH:-./data/pkgs}:/host-packages:ro environment: - - KVM_ENABLED=${KVM_ENABLED:-true} + - KVM_ENABLED=true # The direct path uses /host-packages itself. The KVM path must not # hand that directory to libkrun as a long-lived virtio-fs share. - LAUNCHER_PACKAGES_HOST=/disabled-host-packages - CODEAPI_HARDENED_SANDBOX_MODE=${CODEAPI_HARDENED_SANDBOX_MODE:-true} + # TSI opens guest sockets in this container's network namespace and + # the launcher forwards Docker's resolver into the guest, so the + # guest reaches the egress gateway by service name. - EGRESS_GATEWAY_URL=http://egress_gateway:3190 - SANDBOX_EXECUTE_BODY_LIMIT=${SANDBOX_EXECUTE_BODY_LIMIT:-50mb} - SANDBOX_PACKAGES_DIRECTORY=/pkgs @@ -214,12 +221,12 @@ services: - MINIO_ENDPOINT=minio - MINIO_PORT=9000 - MINIO_USE_SSL=false - - MINIO_ACCESS_KEY=minioadmin - - MINIO_SECRET_KEY=minioadmin + - MINIO_ACCESS_KEY=${S3_ACCESS_KEY:-minioadmin} + - MINIO_SECRET_KEY=${S3_SECRET_KEY:-minioadmin} - FILE_SERVER_PORT=3000 - REDIS_HOST=redis - REDIS_PORT=6379 - - REDIS_PASSWORD=localdev + - REDIS_PASSWORD=${REDIS_PASSWORD:-localdev} - CODEAPI_INTERNAL_SERVICE_TOKEN=${CODEAPI_INTERNAL_SERVICE_TOKEN:-localdev-internal-service-token} depends_on: - minio @@ -228,7 +235,7 @@ services: redis: image: redis:7-alpine container_name: redis - command: redis-server --requirepass localdev --appendonly yes + command: redis-server --requirepass ${REDIS_PASSWORD:-localdev} ports: - ${CODEAPI_REDIS_PORT:-16379}:6379 volumes: @@ -243,8 +250,8 @@ services: volumes: - minio_data:/data environment: - - MINIO_ROOT_USER=minioadmin - - MINIO_ROOT_PASSWORD=minioadmin + - MINIO_ROOT_USER=${S3_ACCESS_KEY:-minioadmin} + - MINIO_ROOT_PASSWORD=${S3_SECRET_KEY:-minioadmin} command: server /data --console-address ":9001" volumes: diff --git a/launcher/entrypoint.sh b/launcher/entrypoint.sh index 47fba9e6..68b203d6 100644 --- a/launcher/entrypoint.sh +++ b/launcher/entrypoint.sh @@ -1,10 +1,11 @@ #!/bin/bash set -e -# TSI opens guest sockets in this container's network namespace. Keep service -# names intact so new connections can resolve replacements after a restart. -# Forward the resolver and search domains supplied by Docker or Kubernetes, -# rather than pinning endpoint IPs or baking a deployment-specific nameserver. +# TSI opens guest sockets in this container's network namespace, but the +# guest cannot use Docker's embedded DNS (127.0.0.11) over TSI, so service +# names are resolved to IPs below, before the microVM boots. The resolver +# directives are still forwarded (SANDBOX_RESOLV_CONF) because guest-dns.sh +# requires a nameserver at boot. # # libkrun places every guest environment entry on the kernel command line, # which accepts only single-line printable ASCII and is truncated by the guest @@ -197,9 +198,65 @@ if [[ "$RESOLV_FIELD_SEPARATOR$SANDBOX_RESOLV_CONF" != *"${RESOLV_FIELD_SEPARATO exit 1 fi -if [ -n "$LOOPBACK_NAMESERVER" ]; then - start_resolver_relay "$LOOPBACK_NAMESERVER" "$RESOLVER_RELAY_ADDRESS" -fi +# Resolve Docker Compose service names to IPs before entering the microVM. +# libkrun's TSI networking does not have access to Docker's embedded DNS +# (127.0.0.11), so DNS-based service discovery does not work inside the +# guest: UDP queries to it are refused and every artifact upload fails +# with artifact_delivery_failed. Resolve here, in the container's network +# namespace, and hand the guest literal IPs. + +resolve_url() { + local var_name="$1" + local url="${!var_name}" + [ -z "$url" ] && return + + local proto="${url%%://*}" + local rest="${url#*://}" + local host_port="${rest%%/*}" + local path="/${rest#*/}" + [ "$rest" = "$host_port" ] && path="" + local host="${host_port%%:*}" + local port="${host_port#*:}" + [ "$host" = "$port" ] && port="" + + echo "$host" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && return + + local ip + ip=$(getent ahostsv4 "$host" 2>/dev/null | awk '{print $1}' | head -1) + if [ -n "$ip" ]; then + local new_url="${proto}://${ip}" + [ -n "$port" ] && new_url="${new_url}:${port}" + new_url="${new_url}${path}" + export "$var_name"="$new_url" + echo "[entrypoint] ${var_name}: ${host} -> ${ip}" + else + echo "[entrypoint] WARNING: could not resolve ${host}; passing hostname through to the guest" >&2 + fi +} + +resolve_host_port() { + local var_name="$1" + local val="${!var_name}" + [ -z "$val" ] && return + + local host="${val%%:*}" + local port="${val#*:}" + + echo "$host" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && return + + local ip + ip=$(getent ahostsv4 "$host" 2>/dev/null | awk '{print $1}' | head -1) + if [ -n "$ip" ]; then + export "$var_name"="${ip}:${port}" + echo "[entrypoint] ${var_name}: ${host} -> ${ip}" + else + echo "[entrypoint] WARNING: could not resolve ${host}; passing hostname through to the guest" >&2 + fi +} + +resolve_url EGRESS_GATEWAY_URL +resolve_url FILE_SERVER_URL +resolve_host_port SANDBOX_FORWARD_TARGET if [ "${LAUNCHER_FILTER_VSOCK_ENOTCONN:-true}" = "true" ]; then # libkrun can emit this benign TSI/vsock teardown line after the guest has