Skip to content
6 changes: 6 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 20 additions & 13 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
71 changes: 64 additions & 7 deletions launcher/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down