Each project gets its own Docker Compose file. These allow you to set up whatever project specific services and configuration you need.
None of your projects need to expose a port. Instead each project gets a unique name like `fooproject` and `barproject` and the container listening to port 80 is named {project-name}-web.
It all gets tied together by a single global NGINX/Traefik/Caddy container (you choose) that exposes port 80 and 443 and reverse proxies to each project's web container using Docker's internal hostnames. In pseudo-code:
https://fooproject.example.site
{
reverse_proxy fooproject- web:80
}
https://barproject.example.site
{
reverse_proxy barproject-web:80
}
The final piece of the puzzle is that the maintainer of DDEV set up a DNS A record for 127.0.0.1 *.ddev.site
You could do something similar yourself using your own domain or locally with DNSMasq.It may seem overcomplicated (and it is complicated). But since it's a de-facto standard and well-maintained, that maintenance burden is amortized over all the users and projects. (To the naysayers, consider: PopOS/Ubuntu are quite complicated, but they're far easier to use for most people than a simpler hand-rolled OS with only the essentials.)