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:

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.

Important

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)

Tip

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.

Important

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

Warning

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 serve
  • path - 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"

Note

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

Info

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

ParameterTypeDescriptionExample
wintegerWidth in pixels?w=800
hintegerHeight in pixels?h=600
fitstringFit mode: max, pad, crop, stretch?fit=crop
fmtstringOutput format: webp, jpg, png?fmt=webp
qintegerQuality (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:

  1. HTTP Handlers - Edge functions that handle HTTP requests
  2. 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.html reads ./dist/index.html
  • Opening /api/data.json reads ./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-wasip2 target (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, longitude
  • req - 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 methods
  • env - Environment variables as key-value pairs (e.g., { API_KEY = "secret" })
  • env_file - Path to file containing environment variables
  • timeout_ms - Execution timeout in milliseconds
  • max_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, longitude
  • info - File information (path, mime-type, size)
  • content - File content as bytes

And return:

  • option<transform-result> - Either the transformed content or null to 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 pairs
  • env_file - Path to file containing environment variables
  • timeout_ms - Execution timeout in milliseconds
  • max_memory_mb - Maximum memory limit
  • max_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 path directory 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.