---
title: "VM Domains"
description: "Route HTTPS traffic from custom domains to services running inside Freestyle VMs."
url: "/docs/vms/domains"
---

VM domains route public HTTPS traffic from a hostname you control to a port inside a Freestyle VM. Create a VM first, then create a domain mapping for the VM port that should receive traffic.


## Domain Flow

1. [Verify ownership](https://www.freestyle.sh/docs/vms/domain-verification) of the domain with a TXT record.
2. [Point DNS](https://www.freestyle.sh/docs/vms/domain-dns) at Freestyle.
3. Map the domain to a VM port.
4. Run a service in the VM that listens on that port.


## Create A VM And Map A Domain

Create the VM, start a service inside it, then map the public hostname to the service port.

```ts
import { freestyle } from "freestyle";

const domain = "app.example.com";

const { vm, vmId } = await freestyle.vms.create();

// Write a small HTTP server into the VM.
await vm.fs.writeTextFile(
  "/root/server.js",
  `
const http = require("http");

http
  .createServer((_req, res) => {
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end("<h1>Hello from a Freestyle VM</h1>");
  })
  .listen(3000, "0.0.0.0");
`,
);

// Install Node and run the server under systemd, so it stays up and restarts.
await vm.exec("apt-get update && apt-get install -y nodejs");
const node = (await vm.exec("command -v node")).stdout!.trim();

await vm.fs.writeTextFile(
  "/etc/systemd/system/app.service",
  `[Service]
ExecStart=${node} /root/server.js
Restart=always
[Install]
WantedBy=multi-user.target`,
);
await vm.exec("systemctl daemon-reload && systemctl enable --now app");

await freestyle.domains.mappings.create({
  domain,
  vmId,
  vmPort: 3000,
});

console.log(vmId);
```


## Map An Existing VM

```ts
await freestyle.domains.mappings.create({
  domain: "app.example.com",
  vmId: "your-vm-id",
  vmPort: 3000,
});
```


## Unmap A Domain

Remove a mapping to stop routing traffic from the domain to the VM. The domain stays verified, so you can map it again later without re-verifying.

```ts
await freestyle.domains.mappings.delete({
  domain: "app.example.com",
});
```


## Requirements

- The domain must be verified before it can be mapped.
- DNS must point at Freestyle before traffic reaches the VM.
- HTTPS is provisioned automatically.
- The service inside the VM must listen on the mapped `vmPort`.
- For HTTP servers, listen on `0.0.0.0`, not only `localhost`.


## Preview Domains

Every account can use `*.style.dev` for free as **preview domains**. Pick any unused `<name>.style.dev` subdomain and map it straight to a VM — a preview domain needs no DNS and no verification (none of the [Requirements](#requirements) above apply), and HTTPS is provisioned automatically. They're ideal for previews, demos, and testing:

```ts
await freestyle.domains.mappings.create({
  domain: `preview-${crypto.randomUUID().slice(0, 8)}.style.dev`,
  vmId,
  vmPort: 8080,
});
```

Bring your own domain when you're ready for production; a `*.style.dev` preview domain is the zero-setup option in the meantime.
