Zeltro › Run multiple projects locally without port conflicts
Running a dozen projects locally without port conflicts
Every project wants port 3000, every compose file wants 3306, and nothing can talk to anything. Here is the arrangement that removes the problem instead of managing it — shared services and one hostname per project.
The problem arrives about the fifth project. Every framework wants port 3000 or
8000. Every docker-compose.yaml you clone wants to bind 3306 or 5432. You start
keeping a note of which project is on which port, and then you need two projects
to talk to each other and discover host.docker.internal.
It is all manageable. It is also avoidable, because the port is only there to disambiguate — and a hostname does that better.
The arrangement#
Each project gets a name, and that name is its address:
http://barber-shop/
http://client-api/
http://internal-docs/
No ports. The same hostname resolves from your browser and from inside any
container, so fetch('http://client-api/') works from another project without a
single line of networking configuration.
Underneath, every project on the machine shares one set of services:
| Service | Container | Reachable as |
|---|---|---|
| MariaDB | zeltro-mariadb |
zeltro-mariadb:3306 |
| Postgres | zeltro-postgres |
zeltro-postgres:5432 |
| Redis | zeltro-redis |
zeltro-redis:6379 |
| MongoDB | zeltro-mongo |
zeltro-mongo:27017 |
| Memcached | zeltro-memcached |
zeltro-memcached:11211 |
Ten projects, one of each. Seven duplicate Postgres containers eating roughly 700 MB becomes one eating about 100.
What this fixes#
Port roulette. Nothing to remember, nothing to allocate, no collisions when two projects both default to 3000.
Cross-project work. A Laravel app reading a Django project's database is a connection string, not a networking exercise. Both see the same hostnames.
Cloned repos that fight you. An upstream compose file binding 5432:5432 or
80:80 gets rewired to the shared services on setup, with the original kept as
docker-compose.upstream.yaml.
Machine load. One database process per engine instead of one per project, which is the difference between a laptop that can hold your whole workload and one that cannot.
Doing it#
zeltro new laravel barber-shop # scaffold, wire up, start
zeltro setup existing-project # adapt a project you already have
zeltro up barber-shop # start it
zeltro ps # what is running, and where
Hostnames are written into /etc/hosts for you. Ports only enter the picture when
you deliberately want to reach a project from another machine on the LAN, and then
each project has a stable published port for exactly that.
Shared services mean shared versions — one Postgres major for everything. If two projects genuinely need different major versions, per-project tooling like DDEV or Lando is the better fit. That is the trade this design makes on purpose.
Why this matters more with an AI agent#
An agent working in a directory has no idea what else is on the machine. Left to
itself it will pick a port, add a database container to the compose file, and
quietly collide with something you had running. A fixed environment — known
hostnames, known services, a written AGENTS.md — is what keeps that from
happening, and it is a large part of why Zeltro is arranged this way.
Questions
Do I still need to remember ports for anything?
Only for reaching a project from a different machine. On the machine itself,
http://project-name/ is the address. zeltro ps prints both the local hostname
and the LAN URL for each project.
How does the hostname resolve?
Zeltro writes entries to /etc/hosts when a project is created, and Docker's
embedded DNS handles resolution between containers on the shared network. The same
name works from both sides.
What if two projects need different Postgres versions?
They cannot have them — one shared instance is the trade. If that is a hard requirement, use per-project tooling for those projects, or keep the odd one out on its own stack.
Does this work with a project that has its own docker-compose.yaml?
Yes. Bundled database services are removed and the app is repointed at the shared
ones, with credentials written into .env. The original file is preserved as
docker-compose.upstream.yaml.
What about the databases themselves — do projects see each other's data?
They share the server, not the database. Each project gets its own database and its own credentials. Cross-project access is possible on purpose, which is what makes a microservice-ish setup easy, but nothing is shared by accident.
Checked 2026-08-22. Other products change often — if something here is out of date, tell us and it gets fixed.