name: supabase-edge-runtime-vps-debug
description: Debug Supabase Edge Runtime functions container on VPS β startup failures, path requirements, Kong routing, and docker-compose validation
Supabase Edge Runtime β VPS Debug & Fix Guide
Context
Self-hosted Supabase on VPS at /docker/deploy-vps/ uses supabase/edge-runtime:v1.58.3 for Edge Functions. The container routes through Kong at 8000 with path /functions/v1/.
Common Container Name
deploy-vps-functions-1
Key Path Requirement
The Edge Runtime starts with:
`
command: ["start", "--main-service", "/home/deno/functions/main"]
`
The main subdirectory is NOT optional. The container expects this exact structure:
`
/home/deno/functions/
main/
index.ts β function code here
`
If the code only exists at /home/deno/functions/manage-users/index.ts but no main/ directory exists, the container will crash immediately with no logs and enter a restart loop.
Debugging Steps
1. Check container status
`bash
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep functions
`
2. Check logs (often empty if crash is immediate)
`bash
docker logs deploy-vps-functions-1 --tail 20
`
3. Check what path actually exists in the volume
`bash
ls -la /opt/rochasales/supabase/functions/
Must show: manage-users/ main/
`
4. Verify the volume mount
`bash
docker inspect deploy-vps-functions-1 --format '{{range .Mounts}}{{.Source}} β {{.Destination}}{{println}}{{end}}'
`
The Fix: Creating the main/ directory
If main/ doesn't exist but manage-users/ does:
`bash
mkdir -p /opt/rochasales/supabase/functions/main
cp /opt/rochasales/supabase/functions/manage-users/index.ts /opt/rochasales/supabase/functions/main/index.ts
`
Then restart:
`bash
cd /docker/deploy-vps && docker compose up -d functions
`
Wait ~20 seconds for Deno to download dependencies and start accepting requests.
Crash Loop Recovery β "Illegal return statement" Error
If the container enters a crash loop with error Uncaught SyntaxError: Illegal return statement at file:///home/deno/functions/main/index.ts:20:3, this error is misleading. The real cause is usually:
1. Network unreachable during boot (can't reach http://kong:8000)
2. esm.sh bundling incompatible ESM modules β the @supabase/supabase-js package via esm.sh may contain transpiled code with syntax incompatible with the Deno runtime version in the container (e.g., iceberg-js with top-level return statements in ESM context)
3. Container tried to start with corrupted cached compiled code
Fix: Remove createClient from manage-users function
If the error persists after clearing the volume, the root cause is an incompatible version of @supabase/supabase-js being bundled by esm.sh at runtime. The solution is to replace createClient with native fetch calls in the isCallerAdmin helper function:
`typescript
// REPLACE this (uses esm.sh which may deliver incompatible bundles):
import { createClient } from "https://esm.sh/@supabase/supabase-js@^2.39.0";
// WITH this (uses Deno's built-in fetch β no external dependencies):
async function createSupabaseClient(supabaseUrl: string, supabaseKey: string) {
// Native fetch β no esm.sh bundling issues
const response = await fetch(${supabaseUrl}/rest/v1/rpc/is_caller_admin, {
method: "POST",
headers: {
"apikey": supabaseKey,
"Authorization": Bearer ${supabaseKey},
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
return response.json();
}
async function isCallerAdmin(supabaseUrl: string, supabaseKey: string): Promise
try {
const result = await createSupabaseClient(supabaseUrl, supabaseKey);
return result === true;
} catch {
return false;
}
}
`
Why this fixes it: The createClient import from esm.sh triggers dynamic bundling of @supabase/supabase-js and its dependencies at container boot time. esm.sh resolves and bundles the latest compatible versions β which may include iceberg-js (a WebSocket library used internally) compiled with syntax that Deno's parser rejects. Using native fetch eliminates all these transitive dependencies.
Then restart with clean volume:
`bash
docker stop deploy-vps-functions-1
rm -rf /opt/rochasales/supabase/functions/main/
cp /path/to/fixed/index.ts /opt/rochasales/supabase/functions/main/index.ts
docker start deploy-vps-functions-1
`
`bash
docker stop deploy-vps-functions-1
rm -rf /opt/rochasales/supabase/functions/main/
Recreate from backup or original
cp /path/to/known-good/index.ts /opt/rochasales/supabase/functions/main/index.ts
docker start deploy-vps-functions-1
`
Critical: Do NOT edit files while the container is in a restart loop β the Edge Runtime caches the first failed parse and keeps crashing before reading the new file content. The volume mount content must be correct before the container starts.
To verify the file inside the container is correct before starting:
`bash
Create a temp container just to read the mount (read-only)
docker create --name func-check -v /opt/rochasales/supabase/functions:/home/deno/functions:ro supabase/edge-runtime:v1.58.3
docker cp func-check:/home/deno/functions/main/index.ts /tmp/verify_index.ts
docker rm func-check
Then compare
diff /tmp/verify_index.ts /path/to/known-good/index.ts
`
Kong Configuration (What Routes to Where)
Kong is configured via declarative YAML at /tmp/kong.yml inside the deploy-vps-kong-1 container. The relevant routing:
`yaml
services:
- name: functions
url: http://functions:9000
routes:
- paths:
- /functions/v1
`
The functions container listens on port 9000 inside the Docker network.
Kong Health Check β 401 on /functions/v1 (Functions Unhealthy)
Kong health-checks the /functions/v1 route without an Authorization header. Since all Edge Functions require auth, Kong receives 401 and marks the service unhealthy.
This does NOT affect external traffic β external calls (browsers, n8n, WhatsApp, etc.) include valid JWT and work fine. Only Kong's internal health metric is wrong.
Fix: Add public health endpoint in main/index.ts
Insert inside Deno.serve(), before any auth checks:
`typescript
Deno.serve(async (req) => {
// Health check (no auth) β must be FIRST, before any auth checks
if (req.method === "GET" && new URL(req.url).pathname === "/functions/v1/health") {
return new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// ... rest of handler (OPTIONS, auth checks, etc.)
`
Why 401 from Kong health check is NOT the same as external 401
| Call | Auth header | Result |
| ------ | ------------- | -------- |
| Kong health check | None | 401 No authorization (from function code) |
| External call | Valid JWT | 200 OK (authenticated) |
| Kong health check | Bearer fake | 401 Unauthorized (token rejected by Supabase) |
The function returns different 401 messages depending on where the request comes from.
Testing the Functions Endpoint
From outside (via nginx β Kong β functions):
`bash
curl -s http://127.0.0.1:8000/functions/v1/ -H 'Authorization: Bearer test'
Returns: {"error":"Unauthorized"} β means Kong is routing, functions is responding
`
Health check endpoint (after fix):
`bash
curl -s http://127.0.0.1:8000/functions/v1/health
Should return: {"status":"ok"}
`
Direct internal health check:
`bash
docker exec deploy-vps-kong-1 sh -c 'wget -O- -q "http://functions:9000/functions/v1/health"'
`
Stats (no restart loop):
`bash
docker stats deploy-vps-functions-1 --no-stream --format 'cpu={{.CPUPerc}} mem={{.MemUsage}}'
If running: cpu=0.00% mem=21.95MiB / 31.34GiB (stable, no restart loop)
`
Why No curl/wget in Container?
The supabase/edge-runtime image is based on deno (Alpine or Distroless) which doesn't include curl or wget. This is normal β you can't exec into it to make HTTP requests.
YAML Corruption Pattern (docker-compose)
The VPS deploy uses Kong with a long command: block. If the depends_on section for kong gets accidentally nested inside the command: block, YAML parsing fails.
Bad (corrupted):
`yaml
kong:
environment:
KONG_PLUGINS: request-transformer,cors,key-auth,acl
command:
- sh
- -lc
- |
cat > /tmp/kong.yml <<'EOF'
...
kong start
db: β THIS IS INSIDE THE COMMAND BLOCK (WRONG)
condition: service_healthy
`
Good:
`yaml
kong:
environment:
KONG_PLUGINS: request-transformer,cors,key-auth,acl
command:
- sh
- -lc
- |
cat > /tmp/kong.yml <<'EOF'
...
kong start
depends_on:
db:
condition: service_healthy
`
Validation:
`bash
python3 -c 'import yaml; yaml.safe_load(open("/docker/deploy-vps/docker-compose.yml")); print("β YAML vΓ‘lido")'
`
Restart Order
When restarting Supabase containers after fixing docker-compose:
`bash
cd /docker/deploy-vps
docker compose up -d db # DB must be healthy first
docker compose up -d kong # Kong must be healthy for others
docker compose up -d functions # Functions last
`
6. --help on the Docker image shows the binary's CLI, not the running server's routes. docker run --rm supabase/edge-runtime:v1.58.3 start --help is for bundling, not runtime
GoTrue Admin Calls β Direct Docker Network vs Kong
Edge Functions calling GoTrue admin endpoints (/admin/users, /admin/users/{id}) should call GoTrue directly on the Docker internal network, NOT through Kong:
`typescript
// β CORRECT β direct Docker network call to GoTrue (port 9999)
const url = http://deploy-vps-auth-1:9999/admin/users;
// β WRONG β goes through Kong, which strips/conflicts headers β 401
const url = ${SUPABASE_URL}/auth/v1/admin/users; // SUPABASE_URL = http://kong:8000
`
Why Kong fails for GoTrue admin calls
Kong's /auth/v1/* route proxies to the gotrue service (GoTrue on port 9999). However, when the Edge Runtime (also inside Docker) calls http://kong:8000/auth/v1/admin/users, Kong's access logs show:
- - Path:
POST /auth/v1/admin/users - - Status: 401 with
{"code":401,"msg":"This endpoint requires a Bearer token"} - -
POST /admin/usersβ create user - -
GET /admin/usersβ list all users - -
PUT /admin/users/{id}β update user - -
DELETE /admin/users/{id}β delete user - -
root-rails-1β main site Rails app - -
root-sidekiq-1β background jobs - -
root-redis-1β cache - -
root-postgres-1β main site DB - -
deploy-vps-db-1β Supabase Postgres - -
deploy-vps-kong-1β Supabase API gateway - -
deploy-vps-auth-1β Supabase Auth - -
deploy-vps-rest-1β Supabase REST - -
deploy-vps-storage-1β Supabase Storage - -
deploy-vps-meta-1β Supabase Meta - -
deploy-vps-studio-1β Supabase Studio - -
n8nβ n8n automation - -
ollama-d0xb-ollama-1β Ollama LLM - -
openclaw-qktd-openclaw-1β Openclaw - -
evolution-apiβ Evolution API
Even though the edge function sends apikey: SERVICE_ROLE_KEY and Authorization: Bearer ${SERVICE_ROLE_KEY}, Kong may not forward these headers correctly to GoTrue, or GoTrue's internal routing rejects the request when it comes through Kong from another container.
Solution: Docker internal network direct call
GoTrue container name on the deploy-vps_default Docker network is deploy-vps-auth-1. Call it directly:
`typescript
const url = http://deploy-vps-auth-1:9999/admin/users;
// ^^^^^^^^ use container name, not kong
`
Headers required for GoTrue admin endpoints
`typescript
const headers: Record
"apikey": SERVICE_ROLE_KEY,
"Authorization": Bearer ${SERVICE_ROLE_KEY}, // BOTH headers required
"Content-Type": "application/json",
};
`
Verify GoTrue is reachable from functions container
From inside the functions container (if it had network tools):
`bash
This would work if the container had curl/wget
curl -s http://deploy-vps-auth-1:9999/health
`
When this matters
The GoTrue admin endpoints are used for:
These are called by the edge function's user management handlers (manage-users, main). Regular PostgREST calls (/rest/v1/*) should still go through Kong since that's the correct path for database access.
Kong access log pattern for this bug
`
172.23.0.9 - - [XX/Jun/2026:HH:MM:SS +0000] "POST /auth/v1/admin/users HTTP/1.1" 401 58 "-" "Deno/1.45.2"
^^^ 401 = GoTrue rejecting via Kong
`
Compare to correct direct call (currently not visible in Kong logs since it bypasses Kong):
`
Would show in functions-proxy logs if it existed, but direct Docker calls don't appear in Kong logs
`
Kong Access Log Location
`bash
Copy from Kong container to inspect requests
docker cp deploy-vps-kong-1:/usr/local/kong/logs/access.log /tmp/kong_access.log
`
Use this to see what requests actually reached Kong and their status codes.
Other Containers on This VPS (Do NOT Touch)
Pitfalls
1. main/ directory missing β container crashes immediately, enters restart loop, no useful logs
2. --main-service path MUST be absolute inside container β /home/deno/functions/main not ./main
3. Kong YAML corruption β depends_on nested inside command: block β YAML parse error
4. Functions needs ~20 seconds after start to download dependencies from esm.sh before accepting traffic
5. No curl/wget in the image β can't exec HTTP requests from inside container
6. --help on the Docker image shows the binary's CLI, not the running server's routes. docker run --rm supabase/edge-runtime:v1.58.3 start --help is for bundling, not runtime