What Splitting a Docker Image Actually Costs You (SPA Routing, CORS, and Five More Surprises)
Description and use
Splitting a single Docker image into separate backend and frontend images exposed hidden issues such as SPA routing, CORS, broken API paths, deployment mismatches, configuration drift, CI/CD complexity, and ambiguous smoke tests. Fixes involved nginx routing, CORS middleware, centralized env vars, updated pipelines, and stricter integration tests.
Problem
Splitting a single Docker image into separate backend and frontend images introduced multiple unforeseen issues such as SPA routing failures, CORS errors, broken API paths, deployment mismatches, configuration drift, CI/CD complexity, and ambiguous smoke tests.
Outcome
The split revealed that cross‑cutting concerns such as routing, CORS, API paths, deployment environments, configuration consistency, and testing must be explicitly addressed for each service. Addressing these issues required additional configuration, tooling, and process changes, but ultimately enabled independent scaling and release cycles.
Workflow
- 1. Create backend/Dockerfile for Go only.
- 2. Create frontend/Dockerfile with Node build stage and nginx final stage.
- 3. Update docker-compose.yml to include separate backend and frontend services.
- 4. Configure nginx with try_files to support SPA routing.
- 5. Add CORS middleware to Go backend with specific AllowOrigins.
- 6. Centralize API base URL in frontend environment variable VITE_API_BASE_URL.
- 7. Consolidate database credentials into a single variable referenced in docker-compose, backend env, and .env.
- 8. Update CI/CD to build and publish both images to GHCR.
- 9. Adjust smoke tests to verify frontend can successfully call backend APIs.
Evidence
When the backend served the frontend directly, a single-page app's routing problem showed up once, and got fixed once.
When one process serves both the API and the frontend, every request is same-origin, the browser never questions it.
The database's username, password, database name, and even host port all live in more than one place now: docker-compose.yml's db service, the backend service's environment override, and a local .env file used when running the backend directly outside Docker.
The seventh consequence: deciding what "done" even means for a two-image smoke test
- Record date: 2026-10-06T04:40:46.361Z