Server Configuration
Nstance Server reads a JSONC configuration file from object storage at shard/{shard}/config.jsonc.
The top-level shape is:
cluster- cluster-scoped identity, storage, secrets, and cluster leader election settings.shard- shard-scoped server runtime, infrastructure provider, subnet, and shard leader election settings.server- files generated for services local to the Nstance server host.load_balancers- load balancers that groups can register instances with.tunnels- provider-neutral wake-tunnel runtimes matching tunnel load balancers.images- optional image lookup configuration.certificates- certificate templates for files generated by the server.defaults- global args, vars, and userdata inherited by templates/groups.templates- instance templates referenced by groups and on-demand instances.groups- tenant-scoped static group configuration.nat- optional tenant-scoped managed NAT configuration.
Below is an example/reference configuration file for Nstance Server, using example data for a fictional AWS-backed Kubernetes cluster:
{
"cluster": {
"id": "example-cluster", // Cluster ID (lowercase alphanumeric + hyphens)
// Optional: separate storage for cluster-scoped data. If omitted, the shard bucket is used with the "cluster/" prefix.
"storage": {
"provider": "s3",
"bucket": "example-nstance-state",
"region": "us-west-2",
"prefix": "cluster/"
},
"secrets": {
// AWS default. Names are appended below this Parameter Store path.
"provider": "aws-parameter-store",
"prefix": "/example-cluster/",
// Optional: secret cache TTL. Omit or set to 0 to disable caching.
"cache_ttl": "5m"
},
// Optional: cluster-wide leader election timings. Omit to use server defaults.
"leader_election": {
"enabled": true,
"frequent_interval": "5s",
"infrequent_interval": "30s",
"leader_timeout": "15s"
}
},
"shard": {
"id": "us-west-2a", // Shard ID (lowercase alphanumeric + hyphens)
"infra": {
"provider": "aws",
"region": "us-west-2",
"zone": "us-west-2a", // aka CAPI "Failure Domain"
"options": {
"profile": "production"
}
},
"bind": {
"health_addr": "0.0.0.0:8990", // HTTP health endpoint for ASG/LB health checks (503 until config is loaded, then 200)
"election_addr": "0.0.0.0:8991", // HTTPS leader election health endpoint for peer-to-peer leader election health checks
"registration_addr": "0.0.0.0:8992", // Agent/Operator registration (anonymous)
"operator_addr": "0.0.0.0:8993", // Operator connections (authenticated)
"agent_addr": "0.0.0.0:8994" // Agent connections (authenticated)
},
"advertise": {
"health_addr": "10.0.0.1:8990", // HTTP health endpoint advertised for ASG/LB health checks and monitoring
"election_addr": "10.0.0.1:8991", // HTTPS leader election health endpoint advertised to peer servers
"registration_addr": "10.0.0.1:8992", // Address used by Nstance Agents and Nstance Operator to register
"operator_addr": "10.0.0.1:8993", // Address used by Nstance Operator to connect
"agent_addr": "10.0.0.1:8994" // Address used by Nstance Agents to connect
// If the host part is empty, "0.0.0.0", or "::", it is auto-detected from the primary network interface.
},
// Optional: stable leader network configuration for shard leadership.
"leader_network": {
// AWS requires both ip and interface_id (ENI ID). Google Cloud uses ip and leaves interface_id empty.
"ip": "10.0.0.100", // Stable leader IP address (ENI private IP for AWS, reserved IP for Google Cloud)
"interface_id": "eni-0abc123def456789" // AWS ENI ID (required for AWS, not used for Google Cloud)
},
"subnet_pools": {
"nat": ["subnet-00000000"], // Public service subnet for NAT VMs
"control-plane": ["subnet-12345678"], // Maps subnet pools to provider subnet IDs
"ingress": ["subnet-23456789"], // Subnets for ingress/load balancer nodes
"workers": ["subnet-87654321", "subnet-abcdef"] // Each key can map to multiple subnets for capacity
},
// Optional: restrict dynamic groups to these subnet pool IDs. Omit or set empty to allow any configured subnet pool.
"dynamic_subnet_pools": ["workers"],
// Optional: omit shard timing fields to use server defaults.
"request_timeout": "30s",
"create_rate_limit": "1s",
"health_check_interval": "60s",
"default_drain_timeout": "5m",
"image_refresh_interval": "6h", // How often to refresh image resolutions
"shutdown_timeout": "10s",
// Optional: omit garbage_collection to use server defaults.
"garbage_collection": {
"interval": "2m", // How often to backfill provider instances and look for dangling ones
"registration_timeout": "5m", // How long to wait for instance registration before terminating as dangling
"deleted_record_retention": "30m" // How long to keep deleted instance records in the local database
},
// Optional: omit expiry to disable age-based expiry.
"expiry": {
"eligible_age": "504h", // 21 days
"forced_age": "720h", // 30 days
"ondemand_age": "168h" // 7 days
},
// Optional: shard leader election timings. Omit to use server defaults.
"leader_election": {
"enabled": true,
"frequent_interval": "5s",
"infrequent_interval": "30s",
"leader_timeout": "15s"
},
// Optional: jittered delay range before exiting after fatal errors. Omit to use server defaults.
"error_exit_jitter": {
"min_delay": "10s",
"max_delay": "40s"
}
},
// Optional: logical backend registration targets that groups can reference by key.
// Entries use provider-specific AWS target group, Google Cloud NEG, or tunnel listener shapes.
"load_balancers": {
"www": {
"provider": "aws",
"target_groups": [{
"arn": "arn:aws:elasticloadbalancing:us-west-2:123412341234:targetgroup/example-www/abcdef1234567890",
"listener_port": 443,
"target_port": 443,
"proxy_port": 8443
}]
},
"web-tunnel": {
"provider": "tunnel",
"listeners": [{"target_port": 80, "proxy_port": 8080}]
}
},
"tunnels": {
"web-tunnel": {
// Example cloudflared tunnel (replace the digest with a real pinned image digest)
"image": "cloudflare/cloudflared@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"args": ["tunnel", "--config", "/run/tunnel/config.json", "run"],
"files": {
"/run/tunnel/config.json": {
"kind": "json",
"template": {
"tunnel": "6ff42ae2-765d-4adf-8112-31c55c1551ef", // replace with a real tunnel ID
"credentials-file": "/run/tunnel/credentials.json",
"metrics": "127.0.0.1:2000",
"ingress": [
{"hostname": "www.example.com", "service": "http://127.0.0.1:8080"},
{"service": "http_status:404"}
]
}
},
"/run/tunnel/credentials.json": {
// Pre-create /example-cluster/tunnel-credentials as a SecureString
// containing the existing tunnel's credentials JSON.
"kind": "secret",
"source": "tunnel-credentials"
}
},
"readiness_url": "http://127.0.0.1:2000/ready",
"readiness_timeout": "30s"
}
},
// Optional: protected files generated for unrelated services local to the server host.
"server": {
"files": {
"service.json": {
"kind": "storage",
"source": "services/local.json"
}
}
},
// Optional: image lookup configuration. Omit if templates use fixed provider image IDs directly.
"images": {
"debian_13_arm64": {
"provider": "aws",
"filters": [
{"name": "name", "values": ["debian-13-arm64-*"]},
{"name": "virtualization-type", "values": ["hvm"]},
{"name": "architecture", "values": ["arm64"]}
],
"owners": ["136693071363"], // Debian
"sort": "creation-date",
"order": "desc",
// Optional: fallback image ID if lookup fails and no cached image is available.
"fallback": "ami-026fccd88446aa0bf"
},
"debian_13_amd64": {
"provider": "aws",
"filters": [
{"name": "name", "values": ["debian-13-amd64-*"]},
{"name": "virtualization-type", "values": ["hvm"]},
{"name": "architecture", "values": ["x86_64"]}
],
"owners": ["136693071363"],
"sort": "creation-date",
"order": "desc",
// Optional: fallback image ID if lookup fails and no cached image is available.
"fallback": "ami-0abcd1234efgh5678"
}
},
// Optional: certificate templates for generated files.
"certificates": {
"kubelet.client": {
"kind": "client",
"cn": "system:node:{{ .Instance.ID }}",
"organization": ["system:nodes"],
"dns": [
"localhost",
"{{ .Instance.ID }}",
"{{ .Instance.Hostname }}"
],
"ip": [
"127.0.0.1",
"::1",
"{{ .Instance.IP4 }}",
"{{ .Instance.IP6 }}"
],
// Optional: certificate TTL in hours. Omit to use server default.
"ttl": 8760
},
"kubelet.server": {
"kind": "server",
"cn": "kubelet",
"dns": [
"localhost",
"{{ .Instance.ID }}",
"{{ .Instance.Hostname }}"
],
"ip": [
"127.0.0.1",
"::1",
"{{ .Instance.IP4 }}",
"{{ .Instance.IP6 }}"
]
},
"kube-apiserver.server": {
"kind": "server",
"cn": "kubernetes",
"dns": [
"localhost",
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local",
"kubernetes.svc.cluster.local",
"{{ .Instance.ID }}",
"{{ .Instance.Hostname }}",
"{{ .Vars.ClusterFQDN }}"
],
"ip": [
"127.0.0.1",
"::1",
"198.18.0.1",
"fdc6::1",
"{{ .Instance.IP4 }}",
"{{ .Instance.IP6 }}"
]
}
},
"defaults": {
"args": {
// Passed to the provider instance creation API. For AWS, this maps to RunInstances.
// Nstance manages instance type, subnet, userdata, and count fields.
"Ipv6AddressCount": 1,
"BlockDeviceMappings": [
{
"DeviceName": "/dev/sda1", // root volume
"Ebs": {
"VolumeSize": 50, // in GB
"VolumeType": "gp3",
"Encrypted": true
}
}
],
"MetadataOptions": {
"HttpEndpoint": "enabled", // default
"HttpProtocolIpv6": "disabled", // default
"HttpPutResponseHopLimit": 1, // default
"HttpTokens": "required", // always require IMDSv2
"InstanceMetadataTags": "enabled"
},
"PrivateDnsNameOptions": {
"EnableResourceNameDnsARecord": true
},
"TagSpecifications": [
{
"ResourceType": "instance",
"Tags": [
{"Key": "InstanceId", "Value": "{{ .Instance.ID }}"},
{"Key": "InstanceKind", "Value": "{{ .Instance.Kind }}"}
]
}
]
},
"vars": {
"ClusterSlug": "example-cluster",
"ClusterFQDN": "example-cluster.cluster.cool",
"Environment": "production",
"CloudBillingID": "123412341234",
"SSHAuthorizedKey": "",
"TelemetryBucket": ""
},
// Optional: global userdata used by templates that do not define userdata.
"userdata": {
"content": "#!/bin/bash\necho \"default userdata for {{ .Instance.ID }}\""
}
},
"templates": {
"nat": {
"kind": "nat",
"arch": "arm64",
"args": {
"ImageId": "{{ .Image.debian_13_arm64 }}", // resolved from images config
"SecurityGroupIds": ["sg-12341234abcd1234ab"],
"IamInstanceProfile": {
"Arn": "arn:aws:iam::123412341234:role/example-instance-role"
}
},
"userdata": {
"content": "#!/bin/bash\necho \"hello from {{ .Instance.ID }}\""
},
// template defaults:
"size": 1,
"instance_type": "t4g.small",
"subnet_pool": "nat",
"vars": {}
},
"knc": {
"kind": "knc", // 3 lowercase letters, used as prefix for instance ID (puidv7)
"arch": "arm64", // used to validate instance type
"files": {
// send secret `tunnel-20250924.json` as `tunnel.json` file
"tunnel.json": {
"kind": "secret",
"source": "tunnel-20250924.json"
},
// send a file from object storage
"bootstrap-config.json": {
"kind": "storage",
"source": "files/{{ .Vars.Environment }}/bootstrap-config.json"
},
// send a certificate generated using a public key sent from the agent
"kubelet.client.crt": {
"kind": "certificate",
"template": "kubelet.client",
"key": {
"source": "agent",
"name": "kubelet.client.pub"
}
},
// send templated environment file
"instance.env": {
"kind": "env",
"template": {
"INSTANCE_ID": "{{ .Instance.ID }}",
"ENVIRONMENT": "{{ .Vars.Environment }}",
"K8S_NODE_LABELS": "{{ .Vars.KUBELET_NODE_LABELS }}",
"CLUSTER_FQDN": "{{ .Vars.ClusterFQDN }}"
}
},
// send templated JSON configuration file
"kubelet-config.json": {
"kind": "json",
"template": {
"kind": "KubeletConfiguration",
"apiVersion": "kubelet.config.k8s.io/v1beta1",
"address": "{{ .Instance.IP4 }}",
"port": 10250,
"cgroupDriver": "systemd",
"clusterDomain": "cluster.local",
"nodeLabels": {
"instance.example.com/id": "{{ .Instance.ID }}"
}
}
},
"custom.conf": {
"kind": "string",
"template": "instance={{ .Instance.ID }}\ncluster={{ .Vars.ClusterSlug }}\n"
}
},
"args": {
"ImageId": "{{ .Image.debian_13_arm64 }}", // resolved from images config
"SecurityGroupIds": ["sg-23412341abcd1234ab"],
"IamInstanceProfile": {
"Arn": "arn:aws:iam::123412341234:role/example-instance-role"
},
// Example of overriding a default arg. Objects are merged recursively; non-object values replace the lower-priority value.
"BlockDeviceMappings": [
{
"Ebs": {
"VolumeSize": 100 // in GB
}
}
]
},
"userdata": {
"source": "url",
"content": "https://example.com/userdata/knc_arm64.sh"
},
// template defaults:
"size": 1,
"instance_type": "t4g.medium",
"subnet_pool": "control-plane",
"vars": {
"K8SAPIHostname": "example-cluster.cluster.cool",
"OIDCIssuer": "https://auth.example.com",
"RegistryBucket": ""
}
},
"knd": {
"kind": "knd",
"arch": "arm64",
"args": {
"ImageId": "{{ .Image.debian_13_arm64 }}"
},
"instance_type": "t4g.large",
"subnet_pool": "workers"
}
},
"groups": {
"default": {
"nat": {
"template": "nat",
"instance_type": "t4g.small",
"subnet_pool": "nat" // Public service subnet pool for NAT VMs
},
"main": {
"template": "knc",
"size": 1,
"subnet_pool": "control-plane",
"vars": {
"KUBELET_NODE_LABELS": "controlplane"
}
},
"ingress": {
"template": "knd",
"subnet_pool": "ingress",
"load_balancers": ["www", "web-tunnel"],
"vars": {
"KUBELET_NODE_LABELS": "traefik"
}
},
"apps": {
"template": "knd",
"size": 2,
"instance_type": "t4g.xlarge",
"subnet_pool": "workers",
"drain_timeout": "10m",
"vars": {}
}
}
},
"nat": {
"default": {
"group": "nat",
"public_addresses": [
{"ipv4": "192.0.2.10", "allocation_id": "eipalloc-0123456789abcdef0"}
],
"last_instance_grace_period": "10m",
"instance_type_ladder": ["t4g.small", "t4g.medium", "t4g.large"],
"scale_up_thresholds": {
"conntrack_percent": 80,
"cpu_percent": 80,
"packet_drops_per_second": 1
},
"scale_down_thresholds": {
"conntrack_percent": 30,
"cpu_percent": 30,
"packet_drops_per_second": 0
}
}
}
}AWS Secrets Manager (aws-secrets-manager) and encrypted object storage (object-storage) are explicit alternatives. For object storage, encryption_key.provider and entries in old_encryption_keys may use aws-parameter-store; set source to a SecureString parameter name such as /nstance/encryption-key. Parameter Store stores raw UTF-8 text (not base64) and standard-tier values are limited to 4 KiB.
cluster
The cluster block defines cluster-scoped configuration shared by all shards:
id- cluster identifier.storage- optional separate cluster-scoped storage. If omitted, the shard bucket is used with thecluster/prefix.secrets- secret store configuration and encryption key settings.leader_election- optional cluster leader election timing settings.
shard
The shard block defines the Nstance Server process and shard-local infrastructure behavior:
id- unique shard identifier.infra- infrastructure provider (aws,google,mock,tmux, orproxmox), region, zone, and provider-specific options.bind- addresses the server listens on.advertise- addresses clients should connect to.leader_network- stable shard leader IP configuration.subnet_pools- maps subnet pool IDs to provider subnet IDs.dynamic_subnet_pools- optional list limiting which subnet pools dynamic groups can use.request_timeout- provider request timeout.create_rate_limit- minimum delay between provider create calls.health_check_interval- agent health check interval.default_drain_timeout- default graceful drain timeout for group scale-down/replacement.image_refresh_interval- image lookup refresh interval.shutdown_timeout- graceful server shutdown timeout.garbage_collection- provider backfill and dangling instance cleanup settings.expiry- optional instance age limits for automatic rotation.leader_election- optional shard leader election timing settings.error_exit_jitter- optional delay range before exiting after fatal startup/runtime errors.
server
server.files defines protected files generated for services local to the Nstance server host. Nstance Server atomically publishes these files under --server-files-dir; it does not send them to the proxy process over its control socket.
Server files support secret, storage, env, json, and string kinds using the same shapes as template files. Agent-key certificate generation is not available because server-local services do not provide agent public keys.
load_balancers
Load balancers are defined at the top level and referenced by group load_balancers lists. Each entry is a logical backend registration target, not necessarily a distinct physical load balancer. For example, separate ingress and controlplane entries may point at different listener backends on the same provider load balancer, or at different load balancers.
AWS load balancers use:
{
"provider": "aws",
"target_groups": [{"arn": "arn:aws:elasticloadbalancing:...", "listener_port": 443, "target_port": 443, "proxy_port": 8443}]
}Google Cloud load balancers use:
{
"provider": "google",
"network_endpoint_groups": ["example-us-central1-a"],
"frontends": [{"ip": "34.10.20.30", "port": 443}]
}Tunnel load balancers use listeners entries containing target_port and proxy_port. Tunnel entries do not invoke infrastructure provider membership APIs.
tunnels
Each tunnel load balancer requires a same-named tunnels entry.
nstance-server tunnel independently loads this configuration and its referenced
files; the server process sends only the tunnel name, desired state, revision,
and renewable lease.
The main example shows an existing tunnel routing web traffic to the local wake proxy. Replace its tunnel ID, hostname, and image digest with your own values; pre-create the referenced credential secret before deployment.
files maps absolute container paths to secret, storage, or json files.
For json, template must be an object and is written literally, without
interpolation. Keep credentials in secret files.
The supervisor owns these files and mounts them read-only. Permissions default
to 0600; --files-mode sets them for
all tunnel files. Use --files-mode=0644 to let containers read files owned by
another user. Host directories use 0700 and manifests use --files-mode; keep
access to host directories and Pod definitions restricted to protect file contents.
Images must declare a numeric, nonroot USER. Containers use that UID/GID,
with a read-only root filesystem, no capabilities, and no privilege escalation.
Readiness URLs must be explicit loopback HTTP addresses. Provider-specific images, arguments, and files belong in deployment configuration, not Nstance code or control messages.
The supervisor checks container state through the runtime’s CRI API
(--runtime-endpoint, default unix:///run/containerd/containerd.sock). Each
start uses a new Pod UID and fresh files, so old containers cannot count as ready.
To stop, it uses a temporary replacement Pod that cannot run the tunnel to
confirm kubelet has stopped the old Pod before deleting files or reporting
STOPPED. If cleanup fails or exceeds 60 seconds, the supervisor exits without
reporting success. Host services still handle process shutdown and crash recovery.
certificates and template files
certificates defines reusable TLS certificate templates. Templates can be referenced by template files entries with kind: "certificate".
Template files support these kinds:
certificate- generate a certificate from a named certificate template. Thekeymust explicitly name a public key sent from the agent.secret- fetch a secret bysourcefrom the configured secrets store.storage- fetch an object bysourcefrom object storage. Thesourcesupports Go template variables.env- render a.envfile from a string-valued template object.json- render a JSON file from a template object.string- render a raw string template for custom formats.
defaults, templates, and groups
defaults, template config, and group config are merged to produce the final instance configuration.
defaults.args,templates.<template>.args, andgroups.<tenant>.<group>.argsare merged with later layers overriding earlier layers.defaults.vars,templates.<template>.vars, andgroups.<tenant>.<group>.varsare merged the same way.defaults.userdatais used when a template does not specify userdata.templatesdefine reusable instance configuration.groupsare nested by tenant:groups.<tenant>.<group>.
For more information see: Image Resolution, Args & Vars Merge Strategy, Args, Vars, and Userdata Templates.
nat
nat.<tenant> enables Nstance NAT instances for that tenant. group is required
and is a singular reference to a group in the same tenant, so at most one NAT
group can be configured. The group must omit size and set its starting instance_type. The
tenant NAT configuration contains instance_type_ladder, ordered from lowest
to highest capacity. CPU, conntrack, and packet-drop thresholds are combined
with scale_up_window (2m-5m),
scale_down_window (20m-30m), cooldown (at least 10m), and
replacement_timeout. Window defaults are 3m, 25m, 10m, and 10m respectively.
Utilization thresholds default to 80% up and 30% down; packet-drop thresholds
default to 1 and 0 drops/second.
public_addresses is optional. When omitted, Nstance routes through each NAT
VM’s ordinary provider interface and its public address may change after
replacement. Each configured item reserves one stable IPv4 egress address for
one populated instance subnet. AWS items contain the Elastic IP’s ipv4 and its
allocation_id; Google Cloud items contain only the reserved external
ipv4. Addresses and allocations cannot be shared between
tenants. last_instance_grace_period defaults to 10m.
The server creates a dedicated NAT instance before the first dependent instance and
removes it only after the subnet remains empty for last_instance_grace_period.
CPU, conntrack, and packet-drop observations drive vertical scaling. Byte and
packet counters remain available for observability but do not affect scaling.
Replacement is create-before-destroy: the new instance starts on its ordinary
interface and must register and report health, the optional fixed IPv4
attachment and route are moved, and only then is the old instance removed. A
replacement reuses the subnet’s existing fixed address and therefore does not
require a spare public IPv4. Exhausting the configured addresses blocks
creation for a new instance subnet but never blocks replacement of an existing NAT
VM. Removing a tenant’s NAT configuration cleans up its NAT instances.
See Network Address Translation for the feature-level design, deployment choices, and NAT64 constraints.