
From One Server to Multi-Service: Splitting with Docker Compose
This site started as a single Next.js app. It was fast, but I wanted to show my Docker and service-architecture skills on my CV. So I split the project into three independent services.
Why split?
A monolith is practical at first, but as it grows three problems appear:
- Frontend and backend run in the same process; if one crashes, the other suffers.
- The database risks being exposed to the outside world.
- Dependencies (Node, Python, Postgres) are crammed into one image.
Three services
The new layout has clear responsibilities:

- front — Next.js (App Router). The only exposed service; it handles the request.
- api — FastAPI + SQLAlchemy. On the internal network only; not directly reachable.
- db — PostgreSQL. Talks only to the API, closed to the outside.
One command to rule them all
docker-compose.yml wires everything together. Thanks to a health check, the API waits until the database is ready:
services:
db:
image: postgres:16-alpine
networks: [internal]
api:
build: ./api
depends_on:
db: { condition: service_healthy }
networks: [internal]
front:
build: ./front
depends_on: [api]
networks: [internal, traefik_default]
Now docker compose up is all it takes. The three services come up in order, find each other on the internal network, and only the frontend is exposed.
Isolated services aren't just safer; they're easier to maintain because you can restart and scale each one independently.
Result
Splitting added some code but clarified the architecture. The database is now fully closed off, the API lives on a private network, and the frontend is the single entry point. In the next post I'll explain how the frontend and API talk to each other securely.