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)
    • 51880 (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.

Observability

Makiatto exports telemetry via OpenTelemetry (OTLP), so you can use any observability stack that supports OTLP ingestion. This guide demonstrates one approach using Grafana and related open-source components. We chose this stack because it's free, self-hosted, well-documented, and widely used, but feel free to use whatever works for you (Datadog, New Relic, Jaeger, etc.).

Architecture

┌────────────┐  ┌────────────┐  ┌────────────┐
│  Makiatto  │  │  Makiatto  │  │  Makiatto  │
│   Node A   │  │   Node B   │  │   Node C   │
└──────┬─────┘  └──────┬─────┘  └──────┬─────┘
       └───────────────┼───────────────┘
                       ▼
           ┌────────────────────────┐
           │       Collector        │
           │     (spanmetrics)      │
           └──┬─────────┬─────────┬─┘
              ▼         ▼         ▼
         ┌───────┐ ┌──────────┐ ┌──────┐
         │ Tempo │ │Prometheus│ │ Loki │
         └───┬───┘ └────┬─────┘ └──┬───┘
             └──────────┼──────────┘
                        ▼
                   ┌─────────┐
                   │ Grafana │
                   └─────────┘

Makiatto nodes send telemetry to a central OpenTelemetry Collector, which routes data to the appropriate backends:

  • Collector - Receives OTLP data, generates RED metrics (rate, errors, duration) via spanmetrics, and forwards to backends
  • Tempo - Stores and indexes distributed traces
  • Prometheus - Stores metrics and provides a query interface
  • Loki - Aggregates and indexes logs
  • Grafana - Visualisation and dashboards for metrics, traces, and logs

This guide runs all components on a single server for simplicity, but you could split them across multiple machines for high availability or to handle higher volumes of telemetry data.

Prerequisites

  • A running Makiatto cluster
  • A separate server for the observability stack (can be any Linux box with a public IP)

Install the following on the observability server:

# Debian/Ubuntu
apt install podman wireguard

# Fedora/RHEL
dnf install podman wireguard-tools

1. Connect to the WireGuard mesh

Makiatto nodes communicate over a private WireGuard network. Add the observability server as an external peer so nodes can send traces securely without exposing the collector publicly.

Generate a WireGuard keypair

On the observability server:

wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
chmod 600 /etc/wireguard/private.key

Register the peer

Copy the public key from the observability server, then from your workstation:

maki peer add o11y \
  --wg-pubkey "PUBLIC_KEY_HERE" \
  --endpoint your-server-ip

The --endpoint should be the public IP or domain name of the observability server (not the WireGuard address).

Info

Name the peer with "o11y" (at the start or end) so Makiatto auto-discovers it as the telemetry endpoint. Examples: o11y, o11y-grafana, metrics-o11y.

This assigns a WireGuard address automatically. To see the full configuration:

maki peer wg-config o11y

Note

Note the Address line (e.g. 10.44.44.5/32) - you'll need this for the collector config.

Configure WireGuard

Save the output of maki peer wg-config o11y as /etc/wireguard/wg0.conf on the observability server, replacing <private-key> with the contents of /etc/wireguard/private.key.

Start the interface:

wg-quick up wg0
systemctl enable wg-quick@wg0

Verify connectivity by pinging a Makiatto node's WireGuard address (e.g. ping 10.44.44.1).

2. Deploy the observability stack

We'll use Podman Quadlets for systemd-managed containers.

Config files

mkdir -p /etc/makiatto-o11y

Create /etc/makiatto-o11y/tempo.yaml:

stream_over_http_enabled: true

server:
  http_listen_port: 3200
  grpc_listen_port: 9095

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317

storage:
  trace:
    backend: local
    local:
      path: /var/tempo/traces
    wal:
      path: /var/tempo/wal

Create /etc/makiatto-o11y/loki.yaml:

auth_enabled: false

server:
  http_listen_port: 3100

common:
  ring:
    kvstore:
      store: inmemory
    replication_factor: 1
  path_prefix: /loki

schema_config:
  configs:
    - from: 2020-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

storage_config:
  filesystem:
    directory: /loki/chunks

limits_config:
  retention_period: 336h

Create /etc/makiatto-o11y/otelcol.yaml:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

connectors:
  spanmetrics:
    namespace: traces.spanmetrics
    dimensions:
      - name: cdn.cache.hit

exporters:
  otlp/tempo:
    endpoint: systemd-tempo:4317
    tls:
      insecure: true
  loki:
    endpoint: http://systemd-loki:3100/loki/api/v1/push
  prometheus:
    endpoint: 0.0.0.0:9464

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [spanmetrics, otlp/tempo]
    metrics:
      receivers: [otlp, spanmetrics]
      exporters: [prometheus]
    logs:
      receivers: [otlp]
      exporters: [loki]

Create /etc/makiatto-o11y/prometheus.yml:

scrape_configs:
  - job_name: makiatto-spanmetrics
    static_configs:
      - targets:
          - systemd-otelcol:9464

Quadlet units

Create these files in /etc/containers/systemd/.

o11y.network:

[Network]

tempo.container:

[Container]
Image=docker.io/grafana/tempo:latest
Exec=-config.file=/etc/tempo/config.yaml
Volume=/etc/makiatto-o11y/tempo.yaml:/etc/tempo/config.yaml:ro
Volume=tempo-data:/var/tempo
Network=o11y.network

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

tempo-data.volume:

[Volume]

loki.container:

[Container]
Image=docker.io/grafana/loki:latest
Exec=-config.file=/etc/loki/config.yaml
Volume=/etc/makiatto-o11y/loki.yaml:/etc/loki/config.yaml:ro
Volume=loki-data:/loki
Network=o11y.network

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

loki-data.volume:

[Volume]

otelcol.container (replace 10.44.44.X with your WireGuard address):

[Container]
Image=docker.io/otel/opentelemetry-collector-contrib:latest
PublishPort=10.44.44.X:4317:4317
Volume=/etc/makiatto-o11y/otelcol.yaml:/etc/otelcol-contrib/config.yaml:ro
Network=o11y.network

[Unit]
Requires=tempo.service loki.service
After=tempo.service loki.service

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

prometheus.container:

[Container]
Image=docker.io/prom/prometheus:latest
Volume=/etc/makiatto-o11y/prometheus.yml:/etc/prometheus/prometheus.yml:ro
Volume=prometheus-data:/prometheus
Network=o11y.network

[Unit]
Requires=otelcol.service
After=otelcol.service

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

prometheus-data.volume:

[Volume]

grafana.container:

[Container]
Image=docker.io/grafana/grafana:latest
PublishPort=3000:3000
Volume=grafana-data:/var/lib/grafana
Network=o11y.network

[Unit]
Requires=prometheus.service tempo.service loki.service
After=prometheus.service tempo.service loki.service

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

grafana-data.volume:

[Volume]

Start the stack

systemctl daemon-reload
systemctl start grafana

The dependencies will start Tempo, Loki, otelcol, and Prometheus automatically. Services restart on boot.

Optional: HTTPS with Caddy

To access Grafana over HTTPS, add Caddy as a reverse proxy.

Remove PublishPort=3000:3000 from grafana.container, then create:

Caddyfile at /etc/makiatto-o11y/Caddyfile:

grafana.example.com {
	reverse_proxy systemd-grafana:3000
}

caddy.container:

[Container]
Image=docker.io/library/caddy:latest
PublishPort=443:443
PublishPort=80:80
Volume=/etc/makiatto-o11y/Caddyfile:/etc/caddy/Caddyfile:ro
Volume=caddy-data:/data
Network=o11y.network

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

caddy-data.volume:

[Volume]

Reload and start Caddy:

systemctl daemon-reload
systemctl start caddy

Caddy automatically obtains a TLS certificate from Let's Encrypt.

3. Restart Makiatto nodes

Restart Makiatto on all nodes:

maki machine restart

Makiatto automatically discovers the OTLP endpoint by finding the external peer named with "o11y" and sets the service name to makiatto.{node_name}.

To override auto-discovery or tune settings:

[o11y]
otlp_endpoint = "http://10.44.44.5:4317"  # Optional: override auto-discovery
sampling_ratio = 0.1       # Sample 10% of traces (default), errors/slow always captured
tracing_enabled = true     # Set to false to disable trace export
metrics_enabled = true     # Set to false to disable metrics export
logging_enabled = true     # Set to false to disable log export

Note

If otlp_endpoint is not set, Makiatto auto-discovers it from external peers. If no o11y peer is found, telemetry export is disabled (console logging still works).

4. Configure Grafana

  1. Sign in at http://<server-ip>:3000 (or your Caddy domain if configured) with admin/admin
  2. Add data sources:
    • Prometheus: http://systemd-prometheus:9090
    • Tempo: http://systemd-tempo:3200
    • Loki: http://systemd-loki:3100

5. Dashboards and alerting

Build dashboards and alerts to suit your needs. Makiatto exports native OpenTelemetry metrics which you can query in Prometheus. Here are some example queries to get started:

PanelQuery
HTTP request ratesum by (service_name) (rate(server_request_count_total[5m]))
Error ratesum(rate(server_request_count_total{http_status_code=~"5.."}[5m]))
p95 latencyhistogram_quantile(0.95, sum(rate(server_request_duration_seconds_bucket[5m])) by (le))

You can also add Tempo trace panels filtered by service.name =~ "makiatto.*" and enable exemplar linking to jump from metric spikes to traces.

For alerting, Grafana supports Prometheus Alertmanager or its own unified alerting system. See the Grafana documentation for more details.

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.