Appearance
Networking & DNS
Every managed service has more than one address, and managed now shows all of them — labeled by where each one is reachable from — instead of only a public IP that often can't connect.
Why the public IP wasn't enough
A database like MySQL declares its port from: private, so the firewall never opens 3306 to the internet. The old connection string — mysql://root:***@<public-ip>:3306 — pointed at an address that would simply time out, and the overlay address you actually needed was impossible to see.
Connection strings
managed apply and managed status now print, per node, every address that service is reachable at:
production-database (primary)
public mysql://root:***@203.0.113.10:3306 not reachable from the public internet — firewalled to your private network
private mysql://root:***@10.100.0.2:3306 WireGuard overlay — reachable from any server in this project
private-dns mysql://root:***@production-database.myproject.managed.internal:3306 stable overlay name — resolves on any server in this projectEach line is tagged by kind:
| Kind | Host | Reachable from |
|---|---|---|
public | public IPv4 | the internet (only if the port is from: public) |
public-dns | <service>.<project>.<account>.by-managed.works | the internet (hosted projects) |
private | overlay/VPC IP | servers on your overlay or VPC |
private-dns | <service>.<project>.managed.internal | servers on your overlay |
The same set is shown on the dashboard and is what managed repl connects over — for a firewalled service it dials the overlay address rather than the dead public one.
Internal DNS (*.managed.internal)
On every server in the overlay, managed publishes a block into /etc/hosts mapping stable names to overlay IPs, regenerated on every apply:
production-database.myproject.managed.internal → 10.100.0.2 (the primary/writer)
production-database-primary.myproject.managed.internal → 10.100.0.2
production-database-replica-1.myproject.managed.internal → 10.100.0.3Names resolve only to overlay addresses, so they work from any server in the project but not from your laptop unless it's on the overlay. The bare <service>.<project>.managed.internal always tracks the writer (primary): when you promote a replica (managed service <name> promote) the name re-points to the new primary right away, and every managed apply reconciles the records too — an app can hardcode the name and never chase an IP again.
<service>is your managed.yaml key (e.g.production-database), not the package name.<project>comes fromname:in managed.yaml. The bare<service>.managed.internalform is also published, so names work even without a project name.
Removals converge too: when a server is destroyed or a service is removed, the remaining servers' overlay peers and *.managed.internal records are updated as part of that same run — including when the fleet shrinks to a single box, whose stale peers and names are cleaned up rather than left behind.
Containers don't see these names
The names live in each server's /etc/hosts, which anything running directly on the server (systemd services, cron jobs, a shell) resolves automatically. Docker and other containers keep their own hosts file, so *.managed.internal does not resolve inside a container. If you run an app in a container, pass the name in explicitly — for example:
docker run --add-host production-database.myproject.managed.internal:10.100.0.2 …— or run the container with --network host, which gives it a copy of the server's /etc/hosts. The copy is taken when the container starts, so restart the container after a promote to pick up the re-pointed names.
Shared addresses (a pair that answers on one IP)
Internal DNS points a name at whichever server holds a role, but a client that has already resolved it — and pooled its connections — doesn't notice when that changes. Where a service must survive losing a server without every client reconnecting somewhere new, two servers can instead share one address.
The address lives on one of them at a time. If that server stops answering, the other takes it over within a few seconds and clients reconnect to the same place. ProxySQL uses this for its virtual_ip; any package can, via managed.vip.ensure() (JS API).
Two constraints, both of which come from what a shared address physically is:
- Same provider, same region. A shared address needs a real shared network segment, where one server can answer for an address that isn't its own. That is the provider's private network. The overlay above is not one — it routes a single address to each host, so an address shared over it belongs to nobody. managed refuses that rather than configuring a group that never fails over.
- The provider has to allow the address. Most clouds drop packets for an address they didn't hand out. Register it on both servers' network ports first — look for "virtual IP", "allowed address pairs", or "anti-spoofing". managed can't see this from inside the server, so an address that never answers is almost always this.
Pick one that's free on your private network and outside any range the provider assigns automatically.
Public DNS (*.by-managed.works)
For services actually exposed to the internet, managed builds a public name under the hosted scheme:
<service>.<project>.<account>.by-managed.works<account> and <project> are the hosted slugs your project is connected to; they're supplied by the control plane when you run managed ui connect. A public name is only shown for a service whose port is from: public (a public name for a firewalled database would be useless) and only on a connected project. Override the parent zone with network.public_dns_zone in managed.yaml if you self-host.
How the records get published
The CLI is the single source of truth for which names exist and where they point: on every apply it writes the desired records (name → public/reserved IP) into state.public_dns_records and syncs them. The control plane (ui/) then publishes that set to the DNS provider — tagging each record per-project so it only touches records it created, and removing records for deleted services.
The only thing that lives outside the code is the DNS zone itself: a domain in Cloudflare and an API token. That's a one-time setup for whoever runs the control plane — see Public DNS with Cloudflare. Until it's configured the CLI still computes and shows the names; the control plane just skips publishing.
The internal *.managed.internal zone needs none of this — it's entirely on-box.