Purpose of this page
Get metals_ui/ on screen in a browser, talking to a real Metals API, without guessing at start order.
Before you start it prerequisites
- Docker Desktop (or Docker Engine plus the Compose plugin) installed and running. No Python or Node is needed for the UI.
- A .env file at the project root with JWT_SECRET_KEY set — Compose refuses to start the API without it, and the UI without an API isn't much use.
- Ports 8888, 5000, and 5432 free on your machine. The first two are configurable via UI_PORT and API_PORT.
Starting it day-to-day
Run from the project root. This starts all three services — database, API, and UI — in dependency order; the UI isn't started until the API reports healthy.
docker compose up -d --build
| Command | What it does |
|---|---|
docker compose up -d --build | Builds the images and starts everything. The --build is what picks up your edits. |
docker compose up -d --build ui | Rebuilds and restarts just the UI — enough after changing anything under metals_ui/. |
docker compose logs -f ui | Nginx's access and error logs, which is where a failing proxy call shows up. |
docker compose ps | Health status of all three services. |
Pointing the UI at an API one variable
The JavaScript never learns the API's address. It always calls the relative path /api, and Nginx decides where that actually goes — from the API_UPSTREAM environment variable.
metals_ui/docker/runtime-config.js// The container serves the UI and proxies API requests on the same origin.
globalThis.METALS_ATLAS_CONFIG = {
apiBaseUrl: "/api",
};
The image copies this over the repository's own runtime-config.js, so the containerized UI always uses the relative path. There's nothing environment-specific in the JavaScript at all.
- Image default — ENV API_UPSTREAM=http://api:5000, so it works with no configuration at all under Compose.
- Compose — restates API_UPSTREAM: http://api:5000 explicitly, so the wiring is visible in one file.
- Azure — an app setting holding the API Web App's real https:// hostname.
To point your local UI at a deployed API, override the variable rather than editing any file: docker compose run -e API_UPSTREAM=https://<api>.azurewebsites.net ui.
Why there's no CORS problem what the proxy buys you
Serving the UI on one port and calling an API on another used to mean two different origins, which triggers the browser's cross-origin rules: preflight requests, required response headers, and blocked responses when anything is missing. The proxy removes the condition that causes all of it.
The page comes from http://localhost:8888 and its API calls go to http://localhost:8888/api/… — same scheme, host, and port. Same-origin, so the cross-origin machinery never engages. Nginx then makes the real call server-to-server, where those browser rules don't apply.
metals_api/app.py still sets Access-Control-Allow-Origin: * on every response. Through the proxy that's simply unused — but it keeps the API callable directly from http://localhost:5000 with curl, Postman, or a browser tab during debugging.
Editing files while it runs the one workflow change
The image COPYs index.html, css/, js/, and assets/ at build time. Editing them on disk changes nothing in the running container until you rebuild — docker compose up -d --build ui. Refreshing the browser first and concluding “my change didn't work” is the single most common confusion here.
If you're making lots of small UI edits, mounting the folder as a bind volume during development is a reasonable local tweak — just don't commit it, since the whole point of the image is that it contains its own files.
Common errors and how to fix them
| What you see | Likely cause | How to fix it |
|---|---|---|
| Page loads, but every API call returns 502 Bad Gateway | Nginx can't reach the API — it's unhealthy, or API_UPSTREAM points somewhere wrong. | docker compose ps to check the API's health, then docker compose logs api. |
| Your edits don't appear after refreshing | The container is still running the previously built image. | docker compose up -d --build ui. |
| 404 for a CSS or JS file | The file wasn't copied into the image, or metals_ui/.dockerignore excluded it. | docker compose exec ui ls /usr/share/nginx/html to see what's actually in there. |
| Port 8888 already in use | Another container or process holds it. | Set UI_PORT in .env to a free port, or stop the other process. |
| Login succeeds but the catalog is empty | The database started without the seed data. | Reset it: docker compose down -v then up -d --build — see the database tutorial. |
| host not found in resolver in the UI's logs | The configured DNS resolver doesn't exist in this environment. | Locally this should be Docker's 127.0.0.11; see the UI image page. |