You’re running into a classic “different data between server and client” hydration problem, but in your case the data is the URL itself (hostname differs between SSR and browser).
From the docs, Nuxt warns that any difference between server-rendered content and client-rendered content will cause hydration mismatch, and recommends using SSR‑friendly composables (useFetch, useAsyncData, useState) so that the same data is reused on the client instead of being recomputed differently. [Hydration reasons; Data fetching]
Also, calling $fetch directly in setup will always refetch on the client, which is exactly what you don’t want here. [$fetch docs]
Given the constraints (different hostnames inside Docker vs in browser), the pattern that is documented is:
- Use a single logical URL in your components (e.g.
/api/foo or '/my-endpoint'), and
- Resolve the actual base URL differently on server vs client, but only in one place, so the data returned is the same.
The Strapi/Docker answer shows exactly this: they compute a different base URL depending on process.server, but the component itself just calls one function, so the response shape is identical on both sides. [Strapi Docker answer]
Example adapted to your case:
export async function useApi<T>(path: string, options?: RequestInit): Promise<T> {
const runtimeConfig = useRuntimeConfig()