feat: proxy backend API through Next.js using runtime BACKEND_URL

Previously, NEXT_PUBLIC_API_URL was a build-time Next.js variable, making
it impossible to configure the backend URL in docker-compose `environment`
without rebuilding the image.

This change introduces a proper server-side proxy:
- next.config.ts: adds a rewrite rule that forwards all /api/* requests
  to BACKEND_URL (read at server startup, not baked at build time).
  Defaults to http://localhost:8000 so local dev works without config.
- api.ts: API_BASE is now an empty string — all fetch calls use relative
  /api/... paths, which the Next.js server proxies to the backend.
- docker-compose.yml: replaces NEXT_PUBLIC_API_URL build arg with a
  runtime BACKEND_URL env var defaulting to http://backend:8000, using
  Docker's internal networking. Port 8000 no longer needs to be exposed.
- README: updates Docker setup docs, standalone compose example, and
  environment variable reference to reflect BACKEND_URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Former-commit-id: b4c9e78cdd
This commit is contained in:
David Parry
2026-03-11 13:49:00 +11:00
parent 3c16071fcd
commit e2a9ef9bbf
5 changed files with 81 additions and 52 deletions
+13
View File
@@ -1,5 +1,10 @@
import type { NextConfig } from "next";
// BACKEND_URL is a plain (non-NEXT_PUBLIC_) env var read at server startup —
// not baked at build time — so it can be set in docker-compose `environment`.
// Defaults to localhost for local dev where both services run on the same host.
const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8000";
const nextConfig: NextConfig = {
transpilePackages: ['react-map-gl', 'mapbox-gl', 'maplibre-gl'],
output: "standalone",
@@ -9,6 +14,14 @@ const nextConfig: NextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
async rewrites() {
return [
{
source: "/api/:path*",
destination: `${backendUrl}/api/:path*`,
},
];
},
};
export default nextConfig;
+1 -1
View File
@@ -592,7 +592,7 @@ export default function Dashboard() {
{backendStatus === 'disconnected' && (
<div className="absolute top-0 left-0 right-0 z-[9000] flex items-center justify-center py-2 bg-red-950/90 border-b border-red-500/40 backdrop-blur-sm">
<span className="text-[10px] font-mono tracking-widest text-red-400">
BACKEND OFFLINE Cannot reach {API_BASE}. Start the backend server or check your connection.
BACKEND OFFLINE Cannot reach backend server. Check that the backend container is running and BACKEND_URL is correct.
</span>
</div>
)}
+7 -28
View File
@@ -1,28 +1,7 @@
// NEXT_PUBLIC_* vars are baked at build time in Next.js, so setting them
// in docker-compose `environment` has no effect at runtime. Instead we
// auto-detect: use the browser's current hostname with a configurable port
// so the dashboard works on localhost, LAN IPs, and custom Docker port maps
// without any code changes.
//
// Override order:
// 1. Build-time NEXT_PUBLIC_API_URL (for advanced users who rebuild the image)
// 2. Runtime auto-detect from window.location.hostname + port 8000
function resolveApiBase(): string {
// Build-time override (works when image is rebuilt with the env var)
if (process.env.NEXT_PUBLIC_API_URL) {
return process.env.NEXT_PUBLIC_API_URL;
}
// Server-side rendering: fall back to localhost
if (typeof window === "undefined") {
return "http://localhost:8000";
}
// Client-side: use the same hostname the user is browsing on
const proto = window.location.protocol;
const host = window.location.hostname;
return `${proto}//${host}:8000`;
}
export const API_BASE = resolveApiBase();
// All API calls use relative paths (e.g. /api/flights).
// Next.js rewrites them at the server level to BACKEND_URL (set in docker-compose
// or .env.local for dev). This means:
// - No build-time baking of the backend URL into the client bundle
// - BACKEND_URL=http://backend:8000 works via Docker internal networking
// - Only port 3000 needs to be exposed externally
export const API_BASE = "";