VPCs let Freestyle VMs communicate over private addresses. Use them for databases, internal services, worker pools, or any group of VMs that should not depend on public domains.
Freestyle VPCs are dual-stack. Every VPC gets an IPv6 /64 and an IPv4 /24,
and members take an address from each — so a service that binds 0.0.0.0, or
a container on a v4-only bridge, is reachable from the rest of the network
without anyone configuring anything.
Create A VPC
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const { vpc, vpcId, data } = await freestyle.vpc.create({
slug: "workspace-network",
cidr: "10.40.0.0/24",
// Membership is not permission: without this rule, members of the network
// do not reach each other at all. See the firewall page.
firewall: { rules: [{ action: "allow", source: {}, destination: {} }] },
});
console.log(vpcId, data.cidr, data.cidrV6);
Both CIDRs are optional: omit cidr and Freestyle derives a /24 out of
10.0.0.0/8, omit cidrV6 and it derives a unique-local /64. Name cidr
yourself when the network has to reach an existing estate over a tunnel and
must not overlap it, or when it needs more than 254 IPv4 members.
The firewall block declares rules with the network,
where a bare endpoint means the network being created — “members reach each
other” is the rule nearly every network wants, and nothing on the network
moves without it.
Attach VMs
A VM can join at most one VPC. Declare the attachment when the VM is created —
pass the VPC ID or your slug for it — or change it later with the
update-networks API (PUT /v5/vms/{vmIdOrSlug}/networks), which attaches,
re-addresses, or detaches declaratively whether the VM is stopped, running, or
paused:
const { vm: apiVm } = await freestyle.vms.create({
// Required: a VM reaches nothing it has not been allowed to.
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
slug: "api",
networks: [{ vpc: vpcId, ipv4: "10.40.0.10" }],
});
const { vm: workerVm, data: worker } = await freestyle.vms.create({
// Required: a VM reaches nothing it has not been allowed to.
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
slug: "worker",
networks: [{ vpc: vpcId, ipv4: true }],
});
console.log(worker.networks[0]?.ipv4, worker.networks[0]?.ipv6);
Both families are allocated by default, so ipv4/ipv6 only matter when you
want to pin an address or opt out. Pass a string to pin one, true to
allocate explicitly, or false to make the member single-family.
Install an in-guest static route when another VPC member is acting as a router:
const { vm } = await freestyle.vms.create({
// Required: a VM reaches nothing it has not been allowed to.
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
networks: [{
vpc: vpcId,
ipv4: "10.40.0.20",
routes: [{ cidr: "172.20.0.0/16", via: "10.40.0.1" }],
}],
});
Test Private Connectivity
Bind destination services to 0.0.0.0 for IPv4, or :: for both address
families, then connect using the address in VmData.networks — worker was
allocated its address rather than pinning one, so read it back rather than
guessing it:
const workerIp = worker.networks[0]?.ipv4;
await workerVm.exec("nohup python3 -m http.server 8080 --bind 0.0.0.0 >/tmp/http.log 2>&1 &");
const result = await apiVm.exec(`curl http://${workerIp}:8080`);
console.log(result.stdout);
This works because the network was created with the members-reach-each-other
rule above; without it, the curl would time out.
Connect From Your Computer
Create a persistent WireGuard peer with tunnels, then use the returned config whenever your computer needs access to the VPC.
Manage VPCs
const { vpcs } = await freestyle.vpc.list();
const current = await freestyle.vpc.get("workspace-network");
await vpc.update({ slug: "private-workers" });
await vpc.delete();
Delete or move every attached VM, and detach every tunnel, before deleting a VPC.