πŸ“„ SKILL.md

← Vault

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

CallAuth headerResult
---------------------------
Kong health checkNone401 No authorization (from function code)
External callValid JWT200 OK (authenticated)
Kong health checkBearer fake401 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:

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