Introduction
Makiatto builds a global CDN by deploying a single Rust binary to servers in different regions. Each node contains an embedded DNS server, WireGuard mesh, and distributed database with automatic SSL certificates and content synchronisation.
Deploy new nodes with maki machine init. Each node detects its coordinates, auto-discovers existing peers through the distributed database, joins the WireGuard mesh, and replicates state. When users query your domain, the DNS server calculates the geographically closest node and returns that IP address. Because nodes operate independently with local state, they continue serving content even during network partitions between peers.
Key Capabilities
Beyond static content delivery, Makiatto supports:
- Dynamic image processing: On-the-fly image resizing, format conversion, and optimisation with query parameters
- WebAssembly functions: Deploy edge functions as HTTP handlers and file transformers that run on all CDN nodes
When to Use Makiatto
Makiatto is not a cost-effective alternative to services like Cloudflare or Fastly. Running multiple VMs globally will cost significantly more than using a commercial CDN. However, Makiatto makes sense when you want:
- Self-sovereignty: You want full control over your infrastructure without third parties seeing your traffic or contributing to internet centralisation
- Censorship resistance: You're serving legal material that CDNs might block (adult material, political speech, etc.)
- Experimentation: You enjoy building and running your own infrastructure
If none of these apply and cost is a primary concern, a commercial CDN is likely a better choice.
Ready to get started? Head to the Getting Started guide to install Makiatto and set up your first CDN node.
Installation
There are several ways to install the Makiatto CLI on your local machine.
Using Cargo
If you have Rust installed, you can install directly from crates.io:
cargo install makiatto-cli # installs as `maki`
If you don't have Rust installed, get it from rustup.rs.
Download from GitHub Releases
You can download pre-built binaries from the releases page.
Download the maki-* files, not the makiatto-* files (those are the daemon binaries for servers).
Choose the correct file for your platform:
maki-x86_64-unknown-linux-gnu.tar.gz- Linux (Intel/AMD)maki-aarch64-unknown-linux-gnu.tar.gz- Linux (ARM64)maki-x86_64-apple-darwin.tar.gz- macOS (Intel)maki-aarch64-apple-darwin.tar.gz- macOS (Apple Silicon)
Then extract and install:
tar -xzf maki-*.tar.gz
chmod +x maki
mv maki /usr/local/bin/
Verify installation
After installation, verify everything is working:
maki --version
Getting Started
This guide will walk you through setting up your first CDN nodes with Makiatto.
Prerequisites
You'll need servers to run your CDN nodes. These can be VPS instances, dedicated servers, or cloud VMs. Each server should have:
- A public IPv4 address (IPv6 is recommended but optional)
- A supported Linux distribution (Ubuntu, Debian, or similar)
- SSH access with sudo privileges (any port)
- These ports open:
- 53 (DNS)
- 80 (HTTP)
- 443 (HTTPS)
- 853 (DNS over TLS)
- 51820 (WireGuard)
We recommend at least 3 servers in different geographic locations. You need 3+ nameservers for proper DNS redundancy, and geographic distribution is the whole point of a CDN! But you can start with just one server to test things out.
Make sure you have the Makiatto CLI installed on your local machine. See the Installation guide if you haven't done this yet.
Setting up your first nodes
Initialise your CDN nodes with the machine init command. This will install the Makiatto daemon on each server and configure them to join the mesh network:
maki machine init vector root@203.0.113.1
maki machine init klukai ubuntu@198.51.100.1
maki machine init tololo admin@192.0.2.1
The command format is maki machine init <name> <user>@<ip>. These names will be used as nameserver hostnames (e.g., vector.ns.example.com), so choose something short and meaningful.
You can verify your nodes are connected:
maki machine list
Creating your project configuration
Create a makiatto.toml file in your project directory:
[[domain]]
name = "example.com"
path = "./dist"
This tells Makiatto which domain to serve and where your static files are located. The path is relative to the makiatto.toml file.
You must use a root domain that you own and control at the registrar level (like example.com), not a subdomain (like blog.example.com). Makiatto needs to be the authoritative nameserver for the entire domain to handle DNS and SSL certificates properly.
Deploying content
With your nodes set up and configuration ready, deploy your content:
maki sync
This command syncs your static files and domain configuration to all nodes in the mesh. Your content is now distributed globally!
DNS setup
For GeoDNS routing to work, you need to configure your domain to use Makiatto's nameservers:
maki dns nameserver-setup
This command will output detailed instructions like this:
Follow these steps to configure your custom nameservers:
:: Step 1: Add glue records to your domain registrar
Glue records tell the internet where to find your nameservers when they're subdomains of your own domain.
Without them, there's a circular dependency: DNS can't find your nameservers to resolve your domain.
Add these as glue records (also called 'name server records' or 'host records') in your domain registrar's control panel.
:: Glue records for example.com:
vector.ns.example.com: 203.0.113.1, 2001:db8::1
klukai.ns.example.com: 198.51.100.1, 2001:db8::2
tololo.ns.example.com: 192.0.2.1, 2001:db8::3
:: Step 2: Set your domain to use your custom nameservers
After adding glue records, you must also tell your registrar to use your custom nameservers instead of their defaults.
In your domain registrar's control panel, change the nameservers for your domain to:
:: Nameservers for example.com:
vector.ns.example.com
klukai.ns.example.com
tololo.ns.example.com
:: Step 3: Testing and verification
After configuration, test your setup with:
dig @vector.ns.example.com example.com
Note: DNS changes may take 24-48 hours to propagate globally.
Once DNS propagates, visitors will be automatically routed to their nearest node.
What's next?
Your CDN is now operational! Check out the Usage Guide for more advanced features like managing multiple domains, updating content, and monitoring your nodes.
Usage Guide
This guide covers the core commands and workflows for managing your Makiatto CDN.
Machine Management
List all configured machines in your profile:
maki machine list
Add an existing Makiatto node to your profile:
maki machine add <user@host>
Upgrade Makiatto binary on machines:
maki machine upgrade [machine names...]
If no machine names are provided, all machines will be upgraded. You can optionally specify --binary-path to use a local binary instead of downloading from GitHub releases.
Remove a machine from the cluster:
maki machine remove <machine-name>
This command will:
- Remove the machine from all peer databases in the cluster
- Stop and disable the makiatto service on the target machine
- Clean up all makiatto files and directories (
/var/makiatto,/etc/makiatto) - Remove the makiatto binary and user
- Update your profile configuration
You'll be prompted for confirmation before removal. Use --force to skip the confirmation prompt:
maki machine remove <machine-name> --force
Machine removal is permanent and cannot be undone. The machine will need to be re-initialised with machine init to rejoin the cluster.
Status
Check cluster health:
maki health
The health command performs comprehensive checks across your entire cluster.
Configuration
Your project configuration lives in makiatto.toml. Here's a complete example:
[[domain]]
name = "example.com"
path = "./dist"
aliases = ["www.example.com", "old-domain.com"]
[[domain.records]]
type = "TXT"
name = "_dmarc" # Creates _dmarc.example.com
value = "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
[[domain.records]]
type = "MX"
name = "@" # @ means the root domain (example.com)
value = "mail.example.com"
ttl = 300
priority = 10
Required fields
name- The domain name to servepath- Path to static files (relative to makiatto.toml)
Optional fields
aliases - Domains that CNAME to your site. When external domains point to your Makiatto domain via CNAME, listing them here tells Makiatto to:
- Obtain SSL certificates for them
- Serve your content when visitors access them
- Common uses: www subdomains, alternative domain names
domain.records - Custom DNS records to add. Useful for:
- Email configuration (MX, SPF, DKIM)
- Domain verification (TXT)
- Subdomains pointing elsewhere
- Custom records beyond what Makiatto auto-creates
Automatic DNS records
Makiatto automatically creates:
- A/AAAA records for your domain (with GeoDNS)
- NS records for your nameservers
- SOA record for the zone
- CAA record for Let's Encrypt
Multiple domains
You can configure multiple domains in one file:
[[domain]]
name = "example.com"
path = "./dist/blog"
[[domain]]
name = "zuccherocat.cafe"
path = "./dist/site"
Each domain must be a root domain that you control at the registrar level. Subdomains like blog.example.com should be handled via DNS records, not as separate domains.
Profile Management
Your machine configuration is stored in a profile (default: ~/.config/makiatto/default.toml). Profiles can be stored anywhere - in your home directory for personal use or in your project repository for team sharing:
# Personal profile in home directory
maki --profile ~/.config/makiatto/zucchero.toml machine list
# Shared profile in project repo
maki --profile ./deployment/zucchero.toml sync
Profiles contain only public configuration data (machine names, SSH targets, WireGuard public keys, IP addresses). Private keys and credentials are never stored in profiles - SSH keys are passed separately via the --ssh-priv-key flag (if needed). This makes profiles safe to commit to version control for team collaboration.
You can also set the MAKIATTO_PROFILE environment variable:
export MAKIATTO_PROFILE=./deployment/defy.toml
maki sync
The --profile flag takes precedence over the environment variable when both are set.
Dynamic Image Processing
Makiatto includes built-in dynamic image processing powered by Imageflow. Transform images on-the-fly by adding query parameters to your image URLs:
https://zuccherocat.cafe/photos/springfield.jpg?w=800&fmt=webp&q=85
Available Parameters
| Parameter | Type | Description | Example |
|---|---|---|---|
w | integer | Width in pixels | ?w=800 |
h | integer | Height in pixels | ?h=600 |
fit | string | Fit mode: max, pad, crop, stretch | ?fit=crop |
fmt | string | Output format: webp, jpg, png | ?fmt=webp |
q | integer | Quality (1-100) | ?q=85 |
Fit modes: max (default, shrink only), pad (letterbox), crop (fill), stretch (distort)
Caching: Processed images are cached in memory (500MB LRU by default) and automatically invalidated when source files change. Images without query parameters are served as static files with no processing overhead.
WebAssembly Functions
Makiatto supports running WebAssembly components as edge functions and file transformers, enabling you to add dynamic functionality to your static sites.
Overview
WASM support in Makiatto provides two types of components:
- HTTP Handlers - Edge functions that handle HTTP requests
- File Transformers - Middleware that transforms files before serving them
Both component types receive node context information, allowing you to write geo-aware and cluster-aware logic.
Runtime Capabilities
The WASM runtime uses WASI (WebAssembly System Interface) with the following capabilities:
- Network access - HTTP requests and database connections to public endpoints (private IPs blocked for SSRF protection)
- File system access - Read-only access to files within the domain directory
- Environment variables - Access to configured env vars via WASI
- Memory limits - Configurable per-function memory limits
- Execution timeouts - Configurable per-function timeouts
File System Sandbox:
WASM functions have read-only access to their domain's directory, sandboxed to prevent accessing other domains or system files. The domain directory is mounted as / inside the WASM environment.
For example, if your domain is example.com with files in ./dist:
- WASM sees
/as the root - Opening
/index.htmlreads./dist/index.html - Opening
/api/data.jsonreads./dist/api/data.json - Cannot access files outside the domain directory or write files
This provides enough capability for edge computing use cases (reading templates, config files, making API calls) while maintaining security. For persistent data, use network calls to external databases or storage services.
Getting Started
First, fetch the WIT interface definitions:
maki wasm fetch
This will download WIT files to ./wit which define the interfaces your component must implement. Build your component using your language's WASM toolchain, for example:
- Rust: wit-bindgen +
wasm32-wasip2target (Rust 1.82+) - Generates bindings and compiles to components - JavaScript: Javy - JavaScript to WebAssembly compiler
- Go: TinyGo + wit-bindgen-go - Go compiler with WIT bindings
- Python: componentize-py - Python bindings and compiler
Node Context
Every WASM function receives information about the node serving the request:
#![allow(unused)] fn main() { struct NodeContext { /// Node name name: String, /// Node geographic latitude latitude: f64, /// Node geographic longitude longitude: f64, } }
This enables use cases like:
- Regional routing decisions
- A/B testing by location
- Custom load balancing logic
- Debug information showing which node served the request
HTTP Handlers
HTTP handlers are WASM components that implement the http world from the Makiatto WIT interface.
Creating a Handler
The function signature is:
handle-request: func(ctx: node-context, req: request) -> response
Where you receive:
ctx- Node context with name, latitude, longitudereq- HTTP request with method, path, query, headers, and body
And return:
response- HTTP response with status code, headers, and body
Example implementation:
package main
import (
"fmt"
// Generated by: wit-bindgen-go generate --world http --out gen ./wit
"gen/makiatto/http/handler"
)
func init() {
handler.Exports.HandleRequest = handleRequest
}
func handleRequest(ctx handler.NodeContext, req handler.Request) handler.Response {
body := fmt.Sprintf("Hello from node %s at (%f, %f)\nPath: %s",
ctx.Name, ctx.Latitude, ctx.Longitude, req.Path)
return handler.Response{
Status: 200,
Headers: []handler.Tuple2[string, string]{
{"content-type", "text/plain"},
},
Body: handler.Some([]byte(body)),
}
}
Deploying a Handler
Add the handler to your domain configuration:
# makiatto.toml
[[domain]]
name = "example.com"
path = "./dist"
[[domain.functions]]
# WASM file at ./dist/api/hello.wasm
path = "api/hello.wasm"
Sync to your cluster:
maki sync
Your function is now available at https://example.com/api/hello
The route is derived from the path - api/hello.wasm becomes /api/hello.
Configuration Options
Required fields:
path- Path to WASM file relative to domain directory (must stay within domain; route derived from this)
Optional fields:
methods- HTTP methods allowed (e.g.,["GET", "POST"]), defaults to all methodsenv- Environment variables as key-value pairs (e.g.,{ API_KEY = "secret" })env_file- Path to file containing environment variablestimeout_ms- Execution timeout in millisecondsmax_memory_mb- Maximum memory limit
File Transformers
File transformers are WASM components that process files before serving them. They implement the transform world.
Creating a Transformer
The function signature is:
transform: func(ctx: node-context, info: file-info, content: list<u8>) -> option<transform-result>
Where you receive:
ctx- Node context with name, latitude, longitudeinfo- File information (path, mime-type, size)content- File content as bytes
And return:
option<transform-result>- Either the transformed content ornullto pass through unchanged
Example implementation:
package main
import (
"fmt"
"strings"
// Generated by: wit-bindgen-go generate --world transform --out gen ./wit
"gen/makiatto/transform/transformer"
"go.bytecodealliance.org/cm"
)
func init() {
transformer.Exports.Transform = transform
}
func transform(ctx transformer.NodeContext, info transformer.FileInfo, content []byte) cm.Option[transformer.TransformResult] {
if strings.Contains(info.MimeType, "html") {
html := string(content)
transformed := fmt.Sprintf("<!-- Served by %s at (%f, %f) -->\n%s",
ctx.Name, ctx.Latitude, ctx.Longitude, html)
return cm.Some(transformer.TransformResult{
Content: []byte(transformed),
MimeType: cm.Some(info.MimeType),
Headers: []transformer.Tuple2[string, string]{
{"x-transformed", "true"},
},
})
}
return cm.None[transformer.TransformResult]()
}
Deploying a Transformer
# makiatto.toml
[[domain]]
name = "example.com"
path = "./dist"
[[domain.transforms]]
# WASM file at ./dist/transform.wasm
path = "transform.wasm"
files = "**/*.html"
Configuration Options
Required fields:
path- Path to WASM file relative to domain directory (must stay within domain)files- Glob pattern matching files to transform (e.g.,**/*.html,**/*.{js,css})
Optional fields:
env- Environment variables as key-value pairsenv_file- Path to file containing environment variablestimeout_ms- Execution timeout in millisecondsmax_memory_mb- Maximum memory limitmax_file_size_kb- Skip files larger than this size
Sequential Transforms
Multiple transforms can be applied to the same file. They execute in the order they appear in your configuration:
[[domain.transforms]]
path = "minify.wasm"
files = "**/*.html"
[[domain.transforms]]
path = "inject-analytics.wasm"
files = "**/*.html"
Each transform receives the output of the previous transform.
FAQ
What operating systems does Makiatto support?
Makiatto only works on Linux. The daemon relies on Linux-specific features for WireGuard, networking, and system integration. You can run the CLI (maki) from macOS or Linux to manage your Linux nodes remotely, but the actual CDN nodes must be running Linux servers. Windows binaries for the CLI are not currently available but may be added in the future.
Why does Makiatto use GeoDNS instead of anycast?
Commercial CDN providers use anycast routing where the same IP address is announced from multiple locations, and internet routing protocols direct users to the nearest server. While this is elegant, it requires:
- Owning IP address blocks (expensive and requires justification)
- BGP peering agreements with multiple ISPs
- Infrastructure in internet exchange points
- Significant technical expertise and relationships
GeoDNS achieves similar results by using GPS coordinates to determine which server is closest to each user. This approach works with regular VPS providers and doesn't require any special infrastructure, making it perfect for self-hosters and small teams who want CDN functionality without enterprise-level requirements.
How many nodes do I need?
Minimum 3 nodes for proper DNS redundancy (you need at least 3 nameservers). For meaningful geographic distribution, you'd want nodes in different regions (e.g., North America, Europe, Asia). You can start with 1 node for testing, but DNS redundancy requires 3+.
Can I use Makiatto with an existing web server?
Currently, Makiatto includes its own web server that listens on ports 80 and 443. It's designed to be the primary web server for your domains.
If you have an existing web server (like nginx or Apache) on the same machine, you would need to:
- Run your existing server on different ports (e.g., 8080/8443), or
- Use dedicated machines for Makiatto (recommended)
Future versions may support reverse proxy configurations, but for now Makiatto expects to own ports 80/443.
Can I host dynamic content or is it only for static files?
Makiatto is primarily designed for static content distribution, but also includes WebAssembly support for dynamic functionality:
Static content:
- Files are synced from your local
pathdirectory to all nodes and served directly - Perfect for websites, documentation, images, and assets
Dynamic content with WebAssembly:
- Edge functions: Deploy HTTP handlers that run on all CDN nodes (similar to Cloudflare Workers)
- File transformers: Process files on-the-fly before serving them (like minification, compression, or adding headers)
See the WebAssembly Functions guide for details on creating and deploying WASM components.
For traditional server-side applications (databases, WebSockets, long-running processes), you may still want to use Makiatto for static assets while keeping your backend on a separate domain and infrastructure.
Can I mix different hosting providers?
Yes! Since nodes communicate via WireGuard mesh over the public internet, you can mix any providers (AWS, DigitalOcean, Hetzner, Vultr, bare metal servers, etc.) as long as they have public IP addresses and allow WireGuard traffic.