Links between VMs
A link lets a program in one of your VMs reach a TCP service in another by name, as in postgres://db:5432/app or fetch("http://api:3000"). The VM with the service is the target, and you publish its port in its folder. The VM that reaches it is the client, and you add the link in the client’s folder. Links work on every backend, whatever the two VMs’ network tiers, as long as the target’s tier is the client’s or a stricter one. devvm carries each connection over ssh through this machine, so neither VM’s network changes, and How it works › Links between VMs explains how.
For a database in ~/code/db and an app in ~/code/app, both with VMs:
cd ~/code/db && devvm link publish 5432 # offer the database's port to links
cd ~/code/app && devvm link add db 5432 # programs in app's VM reach it as db:5432Programs in the app’s VM then reach the database as db:5432 and as db.devvm.internal:5432. A link joins the two VMs’ trust, so add one only between VMs you’d trust with each other’s reach (below). To link several VMs to each other at once, put them in a group.
Publishing a port
Links reach only the ports a VM publishes, so you publish a port in the target’s folder before any VM can link to it:
devvm link publish # what this folder's VM publishes: none, or a list
devvm link publish 5432 # offer port 5432 to links
devvm link publish 8000-8010 # offer a range
devvm link unpublish 5432 # stop offering itdevvm link publishtakes ports and ranges asdevvm portsdoes, such as5432,8000-8010, but notallornone. A port you publish is offered to links from any of your VMs, and each link is still made in its client’s folder withdevvm link add.devvm link unpublishtakes ports asdevvm link publishlists them, and it refuses a port that isn’t published. The server in the VM keeps running. When other VMs link to a port you take out, devvm names them, and their next connections to that port are refused, while connections that are open stay until they close. To remove those links, rundevvm link rmin their folders.- A change applies at once, whether the VM is running or stopped.
- Publishing forwards nothing to this machine, and it doesn’t change the ports the VM forwards.
A link connects to the target’s own 127.0.0.1, so the server in the target has to listen there or on all its addresses. A server that listens on ::1 alone isn’t reached.
Adding a link
In the client’s folder, devvm link add TARGET PORT links its VM to PORT of the VM that TARGET names, once that VM publishes the port:
devvm link add db 5432 # reach port 5432 of the VM for the folder db as db:5432
devvm link add db 5432 --as pg # reach it as pg:5432 instead
devvm link add ../api 8080 # name the target by its folder's path
devvm link add devvm-api-71a99b1076d92c 8080 # or by its VM's nameTARGET takes one of four forms:
- A folder’s name, as VM names have it, such as
dbfordevvm-db-71a99b1076d92c. devvm matches it against the folder that each VM was made for, so a VM whose backend needed a shorter name still matches its whole folder name. When more than one VM has a folder of that name, devvm refuses and lists each VM with its folder, so you can give the VM’s name or the folder’s path instead. - A folder’s path, which starts with
.or holds a/, such as../apior/Users/you/code/api. - A VM’s name, as
devvm lslists it. - A VM’s ssh alias, such as
devvm-db, as you’d give it to ssh (SSH access by name).
devvm refuses a target that has no VM, the client’s own VM, and a port that the target doesn’t publish. It also refuses a link that the two VMs’ tiers don’t allow. When devvm makes the link, it says what the client reaches it as, and that the link joins the two VMs’ trust.
Names
Programs in the client reach a link at NAME:PORT and NAME.devvm.internal:PORT. NAME is the target’s folder name, as VM names have it, unless you give --as NAME. .internal is reserved for private networks, so NAME.devvm.internal never names a host on the internet, and it’s the name to use when the short one could clash with a name your network resolves.
- A client has one name for each VM it links to. The first link to a target sets the name, and later links to that target take it too, so after
devvm link add db 5432 --as pg,devvm link add db 8000reachespg:8000. To change the name, remove that target’s links and add them again. - A name is one DNS label, made of lowercase letters, digits and dashes, which starts and ends with a letter or a digit and isn’t all digits. devvm refuses
localhost, the client’s own hostname and a name that another of the client’s links has. When the target’s folder name isn’t a valid name, or it’s taken, give another with--as NAME. - A link is reached only by its names, never at the client’s
localhost, solocalhost:5432in the client stays the client’s own. - A link’s name hides the same name from anywhere else in the client, such as a host
dbunder your network’s search domain.
Ports
The client reaches the target’s port on the same port number, so devvm link add web 80 gives web:80, and ports below 1024 work like any other. Two targets can publish the same port, because the client reaches each one by its own name.
Links carry TCP over IPv4 alone. Each name resolves to an IPv4 address, so a program that connects only over IPv6 doesn’t reach it, and UDP isn’t carried.
Which VM a link reaches
A link stays with the VM it was made for, because devvm records the target VM’s identity with the link. A VM made again in the target’s folder after devvm rm, or another VM with the same folder name that appears later, isn’t the linked target. devvm layer add, devvm layer rm and devvm update keep the target’s identity as they make the VM again, so its links still reach it (adding layers to a VM).
A link joins the two VMs’ trust
Code in the client can often run code in the target through the target’s own service, such as Postgres’s COPY … TO PROGRAM. It then has whatever the target has: its network, the secrets it may use and its project folder. devvm doesn’t stop that, because it’s what the service itself allows. So add a link only between VMs you’d trust with each other’s reach, and publish only the ports that you mean other VMs to use. Network tiers explains what this means for each tier.
Links and network tiers
A client links only to a target on its own network tier or a stricter one, because a link to a target on a lower tier would let the client reach more through the target. So an allowlist client links to allowlist and none targets, but not to internet or open ones, and an open client may link to any target. When devvm can’t read a client’s or a target’s tier, because that part of its state was changed by hand, it refuses the link rather than take it as open (a file devvm can’t read).
devvm checks this rule when you add a link, at each launch of the client and on every connection. For a client on a confined tier, on every connection the target’s tier must also be in force as the target runs. If you move the target from open to a confined tier while it’s running, devvm ls shows it as unenforced until the target starts again behind its proxy, and devvm refuses the client’s connections to it until then, so start the target again with devvm restart in its folder. devvm also warns you of such a target when you add a link to it, when you publish a port in its folder that the client links to, and each time the client starts. The warning names the target and refuses nothing, and devvm doesn’t warn of a stopped target, because its tier applies when it next starts.
While such a link exists, devvm refuses to give the target a tier lower than a client’s, such as devvm network open in its folder, and names the clients that link to it, and it refuses to give the client a tier higher than a target’s, such as devvm network allowlist for a client that links to an open VM. Remove those links with devvm link rm first, or give the target the client’s tier.
Every VM has its proxy, so a program that uses the VM’s proxy settings, such as curl, sends its requests for a link to the VM’s proxy, and the proxy carries a request for one of the link’s names and its port, such as db:5432 or db.devvm.internal:5432, to the link. Other programs reach the link by its name directly. The proxy passes a link’s connection on as it is, so it doesn’t look into it and puts none of your secrets into it.
The network rules don’t apply to links. The allowlist doesn’t allow or block a link’s connections, and devvm network deny db doesn’t stop the link db. The rule above and the published ports decide which connections a link carries, and devvm network log shows them (the link log). To stop a link, remove it with devvm link rm, or unpublish its port in the target’s folder.
Listing links
devvm link lists this folder’s VM’s links, with one row for each port:
NAME PORT TARGET FOLDER STATUS APPLIES
db 5432 devvm-db-71a99b1076d92c /Users/you/code/db Running yes
db 8000 devvm-db-71a99b1076d92c /Users/you/code/db Running unpublishedSTATUS is the target’s status as its backend reports it, such as Running or Stopped, and it’s empty when devvm can’t tell. APPLIES is yes for a link that works while the target runs, or else it says why the link doesn’t apply:
gone: the target VM no longer exists.replaced: the target’s folder has another VM, made again since the link was.unpublished: the target doesn’t publish the port.policy: the target is on a lower network tier than the client.ungrouped: a group made the link, and the two VMs aren’t in that group together any more. You can remove such a link withdevvm link rm.
devvm ls shows how many links each VM has, in its LINKS column.
devvm link –json
devvm link --json prints the same list as one JSON object, as output for scripts describes. Each of the links has its name, its port, the target VM, the target’s folder as target_folder and its status as target_status, both "" when devvm doesn’t know them, applies, which is true or false, and reason, which is why the link doesn’t apply, as APPLIES shows it, or "" when it does.
{
"version": 1,
"links": [
{
"name": "db",
"port": 5432,
"target": "devvm-db-71a99b1076d92c",
"target_folder": "/Users/you/code/db",
"target_status": "Running",
"applies": true,
"reason": ""
}
]
}devvm link publish –json
devvm link publish --json prints what the VM publishes as one JSON object, and it only shows, so devvm refuses a list of ports with it. ports is none or a list of ports and ranges, written as devvm link publish takes it.
{
"version": 1,
"ports": "5432,8000-8010"
}Removing a link
devvm link rm TARGET PORT removes one of this folder’s VM’s links, where TARGET is the link’s name or anything that devvm link add takes:
devvm link rm db 5432 # stop reaching db:5432
devvm link rm pg 5432 # by the name given with --asThe link’s next connection is refused, while connections that are open stay until they close.
When links change
A change of links reaches a running client at once and a stopped one when it next starts, and devvm says which. On a running client, the names resolve at once, because devvm rewrites the client’s /etc/hosts, and a link’s port answers within a few seconds. The VM’s proxy reaches a new link as soon as its port answers, so the programs that use the VM’s proxy settings also reach it, including those that were already running. Nothing in the VM restarts, and the VM’s environment doesn’t change, whatever its tier.
When a target stops or goes
devvm never starts a VM for a link. A connection to a stopped target fails, and each launch of the client warns about each stopped target, until you start it with devvm in its folder. The client’s links then work again without any change to the client. When the target stops, the connections that were open through it end. A link also needs both VMs’ backends to carry links, which every backend of devvm’s does, and when one can’t, each launch of the client says so.
devvm rm of a target removes every link to it from your other VMs and names the clients that lost one. A VM that you make again in that folder isn’t linked, so publish its ports and add the links again. devvm rm of a client removes its links, and devvm stop of a client ends its links’ connections.
A link can’t listen when a program in the client already holds its port in a way that covers the link’s address, such as a server of the client’s own on all its addresses with the same port. That link alone doesn’t work, devvm says so, and the client’s other links work. Stop that program, then run devvm restart.
Groups
A group is a set of your VMs that reach each other’s published ports by name, without a devvm link add for each pair. You join each VM in its own folder, and devvm makes the links between them, so every VM in the group gets a link to each port that every other VM in it publishes. A VM is in one group at a time.
For a database in ~/code/db, an API in ~/code/api and an app in ~/code/app, each with a VM:
cd ~/code/db && devvm link publish 5432 && devvm group join web
cd ~/code/api && devvm link publish 8080 && devvm group join web
cd ~/code/app && devvm group join webPrograms in the app’s VM then reach db:5432 and api:8080, programs in the API’s VM reach db:5432, and programs in the database’s VM reach api:8080. The links a group makes are links like any other, so what this page says about names, ports, network tiers, the link log and stopped targets holds for them too, and devvm link lists them.
Joining a group
devvm group join NAME puts this folder’s VM in the group NAME. The first VM to join makes the group, and devvm says so. A group’s name is made of lowercase letters, digits and dashes, starts and ends with a letter or a digit, and isn’t all digits.
devvm group join web # join the group web
devvm group join web --as api2 # join it as api2 instead of this folder's name
devvm group # the group, its VMs and the ports each one publishes
devvm group leave # leave itWhen the VM joins, devvm makes the group’s links in it and in each of the group’s other VMs, and it names the links that each VM gains. The links reach a running VM at once and a stopped one when it next starts, as When links change describes. devvm refuses a VM that’s in another group already, so run devvm group leave first, and it refuses a VM whose backend can’t carry links. A VM made again in a folder isn’t in the group that the folder’s old VM was in.
devvm group shows the group this folder’s VM is in, with one row for each of its VMs:
NAME VM FOLDER PUBLISHED
api devvm-api-0f3c2a91b7d4e6 /Users/you/code/api 8080
app devvm-app-5e1d7c08a2b9f4 /Users/you/code/app none
db devvm-db-71a99b1076d92c /Users/you/code/db 5432NAME is what the group’s other VMs reach that VM by, and PUBLISHED is the ports it publishes, or none. When this folder’s VM isn’t in a group, devvm says so.
Ports in a group
The group’s links reach every port that a VM in it publishes, so you publish and unpublish ports in each VM’s folder, as Publishing a port describes. A port that you publish reaches the links of every other VM in the group, and a port that you take out goes from them, at once on a running VM and at its next start on a stopped one. devvm names the links that each VM gains or loses. A range that you publish gives a link for each port in it.
Names in a group
The group’s other VMs reach a VM by its folder name, as VM names have it, unless it joined with --as NAME. The name follows the rules for link names, and devvm refuses localhost.
- Two VMs in a group can’t have the same name, so when another VM in the group has the name this VM would join as, devvm refuses the join until you give this VM another with
--as NAME. devvm also refuses a name that’s another VM’s folder name, because that’s the other VM’s own hostname. To change a VM’s name in its group, leave the group, then join it again with--as NAME. - A VM’s folder name is its own hostname, so devvm refuses to put a VM in a group where another VM’s name is this VM’s folder name, whatever
--as NAMEyou give, because the name would reach the joining VM itself. For two folders with the same name, such as~/work/apiand~/old/api, have the VM that’s in the group leave it and join again with another name, such asdevvm group join web --as api1, then join the other with--as api2. A VM whose folder has a name of its own needs neither. - The links that you make with
devvm link addtake precedence over the group’s. When you link to a VM in the group yourself, the group’s links to that VM take the name your link gives it, and a port that you linked gets no link from the group. When one of your links has the name of another VM in the group, the group’s links to that VM go, and devvm warns you of it, until you remove your link withdevvm link rm.
A group joins the trust of all its VMs
A group joins the trust of every VM in it, as a link joins the two VMs’ trust, because code in any of them can often run code in the others through the ports they publish. A VM that joins later gets the same reach into every VM in the group, and they get the same reach into it, so devvm says so on every join. Put VMs in a group only when you’d trust each of them with the others’ reach.
Groups and network tiers
A group’s VMs are all on one tier. devvm refuses to put a VM in a group whose VMs are on another tier, and it names both tiers. It checks joins one at a time, so when two VMs join a group at once, the second is checked against the first, and a devvm that waits for another says so. While a VM is in a group, devvm refuses to change its tier with devvm network, so run devvm group leave first.
Because every VM in a group is on one tier, the group’s links keep the rule that a client links only to a target on its tier or a stricter one. Each of their connections is still refused while the target’s tier isn’t in force as it runs, and devvm warns you of such a target when the group makes a link to it.
Leaving a group
devvm group leave takes this folder’s VM out of its group. It removes the links that the group made, in this VM and in the group’s other VMs, and names the links that each VM loses. The links that you made with devvm link add stay. devvm link rm doesn’t remove a link that the group made, so leave the group, or unpublish the port in its target’s folder.
devvm rm of a VM takes it out of its group, and when the group’s last VM leaves, the group is gone.
devvm group –json
devvm group --json prints the group as one JSON object, as output for scripts describes. group is the group’s name, or "" when the VM is in none, and members is [] then. Each of the members has its name in the group, its vm, its folder, and the ports it publishes as published, written as devvm link publish lists them, or none.
{
"version": 1,
"group": "web",
"members": [
{
"name": "db",
"vm": "devvm-db-71a99b1076d92c",
"folder": "/Users/you/code/db",
"published": "5432"
}
]
}Containers in the VM
A container that the docker layer runs in the client has a network of its own, so it neither resolves a link’s names nor reaches its address. Run it with --network host, as in docker run --network host IMAGE, so it shares the VM’s network and reaches the links as the VM does.
The link log
devvm network log in the client’s folder shows its links’ connections, whatever its tier. A line has the time, link for a connection devvm carried or REFUSED for one it refused, the link’s name and port, and the target VM, with why devvm refused it:
2026-11-01T10:00:00Z link db:5432 to devvm-db-71a99b1076d92c
2026-11-01T10:05:00Z REFUSED db:5432 to devvm-db-71a99b1076d92c: it isn't runningWhen the client is on a confined tier and the target’s isn’t in force yet, the line says unenforced, as Links and network tiers describes.
A client has at most 256 of its links’ connections carried at once, and devvm refuses and logs the next one. devvm network log –json describes the events in JSON.