Overlord: Deploy jails as fast as you code
Apr 23, 2025 - ⧖ 79 minWhen AppJail, a BSD-3 licensed open source framework entirely written in sh(1)
and C to create isolated, portable and easy to deploy environments using FreeBSD jails that behave like an application, was created, my intention was to test ports so as not to mess up my main environment. Today AppJail is more than just a script for testing ports, it is highly flexible and has some very useful automation features.
After AppJail has reached stability and is used on a variety of systems, I have faced that just deploying a jail for every service I wanted is not feasible, especially when more and more services need to be deployed. Thus Director was born.
AppJail Director is a tool for running multi-jail environments on AppJail using a simple YAML specification. A Director file is used to define how one or more jails that make up your application are configured. Once you have a Director file, you can create and start your application with a single command: appjail-director up
.
Director is the first attempt to follow the "everything is code" philosophy related to AppJail. Director organizes jails into projects, so you create a project with one or more jails declaratively, and Director takes into account any changes you have made to that file or a related file (such as the Makejail used). If Director has seen a change, it doesn't hesitate to destroy your jail to recreate it. This sounds a bit crazy, but is best explained in The Ephemeral Concept:
Director treats each jail as ephemeral. This does not mean that your jails will not persist after you stop them or restart your system, what it means is that Director assumes that it is safe to destroy the jails since you have clearly separated the data that should be persisted from the data considered ephemeral.
There are more details in the appjail-ephemeral(7)
man page, but the principle is the same as above.
Director by itself does not deploy jails, it needs instructions that perform configuration, package installation, etc., so it heavily exploits a feature of AppJail called Makejails, a simple text file that automates the steps of creating a jail. There are many created in the Centralized Repository, but nothing prevents you from using your own repository to host your Makejails.
Both AppJail and Director simplify my life a lot, however you have to deal with a problem that neither AppJail nor Director solve, which is orchestrating jails on many servers. AppJail and Director combined with SSH may be workable for a few servers, but when you have more and more, this is just painful. Thus Overlord was born.
Overlord is a fast, distributed orchestrator for FreeBSD jails oriented to GitOps. You define a file with the service intended to run on your cluster and deployment takes seconds to minutes.
Fortunately for Overlord (and for me), AppJail and Director are mature, so it's a smart move to reuse those extensively tested tools and combine them with Overlord. This is what I have done and borrowing the same philosophy from Director that "everything is code" is why the orchestrator is easy to use. Another decision I have made is that everything in Overlord is asynchronous. Deployments can take a long time when the service is huge, but even when the deployment only takes a little while, it's much better to send instructions declaratively and let Overlord handle our work. In this article, in more detail, we'll look at many of the things I've said here.
Architecture
The Overlord architecture is described as a tree chain architecture. Each Overlord instance running the API server can be configured to group other chains. Each member can be further configured to group more chains together. However, although this step can be performed almost infinitely, doing this without caution can introduce latency, so it is important to know how you plan to organize your servers.
The reason for choosing this architecture is because it is very simple and scales very well, so sharing resources among many servers is just sticking each chain together forming a cluster to deploy projects.
This architecture also abstracts the way projects are deployed. A user who wants to deploy a project does not need to know the endpoint of each chain, only the first one (also known as the root chain) is enough. This is because each chain is tagged with an arbitrary string, so a user only needs to specify in his deployment file the endpoint of the root chain, the access token and the labels. Although labels are subjective, this can represent a desire. For example, we can label servers with the string vm-only
for those servers that have the ability to deploy virtual machines or db-only
for databases, it is really very arbitrary.
bravo
/
main <---> alpha
\
charlie <---> delta
Assume that only charlie and delta have the db-only
label. To deploy projects to the API servers with the specified labels, the client must make an HTTP request to main, specifying the chain alpha.charlie
and alpha.charlie.delta
. This is done transparently and does not require user intervention.
main . alpha . charlie
&
main . alpha . charlie . delta
Smart Timeouts
What happens if a chain is down? If the root chain is down, we can do nothing, although we can specify more than one (although in the rest of this document we will only use one). However, if a chain after the root chain is down something interesting happens.
When a chain positioned after the root chain is down, the chain that detects the error can blacklist the failed chain for a while. The blacklist is used to not display failed chains, although it is not forbidden to attempt to connect to a failed chain through another chain, this is because successfully connecting to a blacklisted chain automatically re-enables it.
Each blacklisted chain has an assigned time to remain on the blacklist. After a period of time, the chain is removed from the blacklist, however, if the chain continues to fail, it is added back to the blacklist but for a longer time. This time increment has a limit so as not to disable a chain forever.
Doing the above has the good effect that an HTTP request can decrease the time to complete since there is no need to connect to a failed chain.
Deploying Projects
The best way to demonstrate Overlord is to deploy a small project.
Note that a project can have more than one jail, however, in the following example only one jail is needed.
filebrowser.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
projectName: filebrowser
projectFile: |
options:
- virtualnet: ':<random> default'
- nat:
services:
filebrowser:
makejail: 'gh+AppJail-makejails/filebrowser'
volumes:
- db: filebrowser-db
- log: filebrowser-log
- www: filebrowser-www
start-environment:
- FB_NOAUTH: 1
arguments:
- filebrowser_tag: 14.2
options:
- expose: '8432:8080 ext_if:tailscale0 on_if:tailscale0'
default_volume_type: '<volumefs>'
volumes:
db:
device: /var/appjail-volumes/filebrowser/db
log:
device: /var/appjail-volumes/filebrowser/log
www:
device: /var/appjail-volumes/filebrowser/www
You have noticed that I'm not specifying the access token and entry point explicitly, but through environment variables, which are loaded through the .env
file:
.env:
ENTRYPOINT=http://127.0.0.1:8888
TOKEN=<access token>
And now another question: how is the access token generated? This is easy, the token is generated from the machine running the Overlord instance you want to contact, however only who has the privileges to access the secret key (which is generated pseudo-randomly by default) can generate tokens.
# OVERLORD_CONFIG=/usr/local/etc/overlord.yml overlord gen-token
...
The next step is simply to apply the deployment file.
$ overlord apply -f filebrowser.yml
If there is no output, everything is fine, however this does not mean that the project is deployed. When a deployment file is applied and contains the specification to deploy a project (in the above case a directorProject
), it is queued waiting for its turn. However, since there are no other projects currently running, our project will be deployed as fast as possible.
$ overlord get-info -f filebrowser.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
filebrowser:
state: DONE
last_log: 2025-04-22_17h57m45s
locked: False
services:
- {'name': 'filebrowser', 'status': 0, 'jail': 'e969b06736'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 7 minutes and 42.41 seconds
job_id: 14
restarted: False
labels:
error: False
message: None
Metadata
Metadata is used to create small files (such as configuration files) that can be used when deploying projects or VMs. While a git hosting such as GitLab, GitHub, Gitea, etc., is very useful in combination with Makejails, you can use Metadata instead of relying on a git hosting to further configure the service or VM you are deploying.
The other advantage of metadata is that it can be shared between different deployments. For example, by deploying virtual machines that share the same sshd_config(5)
and authorized_keys
files.
tor.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
projectName: tor
projectFile: |
options:
- virtualnet: ':<random> address:10.0.0.50 default'
- nat:
services:
tor:
makejail: !ENV '${OVERLORD_METADATA}/tor.makejail'
volumes:
- data: '/var/db/tor'
volumes:
data:
device: '/var/appjail-volumes/tor/data'
metadata.yml:
kind: metadata
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
metadata:
tor.makejail: |
OPTION start
OPTION overwrite=force
INCLUDE gh+DtxdF/efficient-makejail
PKG tor
CMD echo "SocksPort 0.0.0.0:9050" > /usr/local/etc/tor/torrc
CMD echo "HTTPTunnelPort 0.0.0.0:9080" >> /usr/local/etc/tor/torrc
SERVICE tor oneenable
SERVICE tor start
.env:
ENTRYPOINT=http://127.0.0.1:8888
TOKEN=<access token>
There is no difference between deploying a project and the metadata from the user's point of view. However, metadata is not queued, it is simply written (asynchronously) to disk.
$ overlord apply -f metadata.yml
$ overlord apply -f tor.yml
$ overlord get-info -f metadata.yml -t metadata
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
metadata:
tor.makejail: |
OPTION start
OPTION overwrite=force
INCLUDE gh+DtxdF/efficient-makejail
PKG tor
CMD echo "SocksPort 0.0.0.0:9050" > /usr/local/etc/tor/torrc
CMD echo "HTTPTunnelPort 0.0.0.0:9080" >> /usr/local/etc/tor/torrc
SERVICE tor oneenable
SERVICE tor start
$ overlord get-info -f tor.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
tor:
state: UNFINISHED
last_log: 2025-04-22_18h40m30s
locked: True
services:
- {'name': 'tor', 'status': 0, 'jail': '7ce0dfdcef'}
up:
operation: RUNNING
last_update: 38.01 seconds
job_id: 16
Deploying FreeBSD VMs
Overlord can deploy virtual machines thanks to the great vm-bhyve project. A virtual machine isolates many parts that a jail cannot, with the overhead that such a thing implies, however that overhead is not a problem depending on what you are doing.
This deployment works as follows: A director file is created (overlord does this internally), which is used to further create a jail that represents the environment that must have vm-bhyve installed, must be configured to use the firewall (one supported by FreeBSD) and must be configured with the bridge used by the VMs. This sounds really complicated, but there is a Makejail that does it, so take a look at it for details. The Makejail mentioned above creates the environment with vm-bhyve-devel installed, configures pf(4)
and creates a bridge with an assigned IPv4 (192.168.8.1/24
), so we must assign our VM an IPv4 between that range. pf(4)
is not configured to isolate further connections, so an application inside the VM can "escape" to other services, which may or may not be desirable depending on what it is doing.
vm.yml:
kind: vmJail
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- !ENV '${DST}'
vmName: !ENV '${VM}'
makejail: 'gh+DtxdF/vm-makejail'
template:
loader: 'bhyveload'
cpu: !ENV '${CPU}'
memory: !ENV '${MEM}'
network0_type: 'virtio-net'
network0_switch: 'public'
wired_memory: 'YES'
diskLayout:
driver: 'nvme'
size: !ENV '${DISK}'
from:
type: 'components'
components:
- base.txz
- kernel.txz
osArch: amd64
osVersion: !ENV '${VERSION}-RELEASE'
disk:
scheme: 'gpt'
partitions:
- type: 'freebsd-boot'
size: '512k'
alignment: '1m'
- type: 'freebsd-swap'
size: !ENV '${SWAP}'
alignment: '1m'
- type: 'freebsd-ufs'
alignment: '1m'
format:
flags: '-Uj'
bootcode:
bootcode: '/boot/pmbr'
partcode: '/boot/gptboot'
index: 1
fstab:
- device: '/dev/nda0p3'
mountpoint: '/'
type: 'ufs'
options: 'rw,sync'
dump: 1
pass: 1
- device: '/dev/nda0p2'
mountpoint: 'none'
type: 'swap'
options: 'sw'
dump: 0
pass: 0
script-environment:
- HOSTNAME: !ENV '${HOSTNAME}'
script: |
set -xe
set -o pipefail
. "/metadata/environment"
sysrc -f /mnt/etc/rc.conf ifconfig_vtnet0="inet 192.168.8.2/24"
sysrc -f /mnt/etc/rc.conf defaultrouter="192.168.8.1"
sysrc -f /mnt/etc/rc.conf fsck_y_enable="YES"
sysrc -f /mnt/etc/rc.conf clear_tmp_enable="YES"
sysrc -f /mnt/etc/rc.conf dumpdev="NO"
sysrc -f /mnt/etc/rc.conf moused_nondefault_enable="NO"
sysrc -f /mnt/etc/rc.conf hostname="${HOSTNAME}"
if [ -f "/metadata/resolv.conf" ]; then
cp -a /metadata/resolv.conf /mnt/etc/resolv.conf
fi
if [ -f "/metadata/loader.conf" ]; then
cp /metadata/loader.conf /mnt/boot/loader.conf
fi
if [ -f "/metadata/zerotier_network" ]; then
pkg -c /mnt install -y zerotier
zerotier_network=`head -1 -- "/metadata/zerotier_network"`
cat << EOF > /mnt/etc/rc.local
while :; do
if ! /usr/local/bin/zerotier-cli join ${zerotier_network}; then
sleep 1
continue
fi
break
done
rm -f /etc/rc.local
EOF
sysrc -f /mnt/etc/rc.conf zerotier_enable="YES"
elif [ -f "/metadata/ts_auth_key" ]; then
pkg -c /mnt install -y tailscale
ts_auth_key=`head -1 -- "/metadata/ts_auth_key"`
echo "/usr/local/bin/tailscale up --accept-dns=false --auth-key=\"${ts_auth_key}\" && rm -f /etc/rc.local" > /mnt/etc/rc.local
sysrc -f /mnt/etc/rc.conf tailscaled_enable="YES"
fi
if [ -f "/metadata/timezone" ]; then
timezone=`head -1 -- "/metadata/timezone"`
ln -fs "/usr/share/zoneinfo/${timezone}" /mnt/etc/localtime
fi
if [ -f "/metadata/sshd_config" ]; then
sysrc -f /mnt/etc/rc.conf sshd_enable="YES"
cp /metadata/sshd_config /mnt/etc/ssh/sshd_config
fi
if [ -f "/metadata/ssh_key" ]; then
cp /metadata/ssh_key /mnt/etc/ssh/authorized_keys
fi
if [ -f "/metadata/sysctl.conf" ]; then
cp /metadata/sysctl.conf /mnt/etc/sysctl.conf
fi
if [ -f "/metadata/pkg.conf" ]; then
mkdir -p /mnt/usr/local/etc/pkg/repos
cp /metadata/pkg.conf /mnt/usr/local/etc/pkg/repos/Latest.conf
fi
metadata:
- resolv.conf
- loader.conf
- timezone
- sshd_config
- ssh_key
- sysctl.conf
- pkg.conf
- ts_auth_key
metadata.yml:
kind: metadata
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- vm-only
metadata:
ts_auth_key: '<tailscale auth key>'
resolv.conf: |
nameserver 192.168.1.107
timezone: 'America/Caracas'
loader.conf: |
nvme_load="YES"
if_bridge_load="YES"
bridgestp_load="YES"
if_wg_load="YES"
kern.racct.enable=1
ssh_key: '<SSH public key>'
sshd_config: |
# Ports
Port 22
# Authentication
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitRootLogin prohibit-password
PrintMotd no
# Forwarding
X11Forwarding no
AllowAgentForwarding yes
# Connection checks
ClientAliveCountMax 3
ClientAliveInterval 15
# Compression
Compression no
# Limits
LoginGraceTime 40
# Public keys
AuthorizedKeysFile /etc/ssh/authorized_keys
# SFTP
Subsystem sftp internal-sftp
sysctl.conf: |
# A bit of hardening
security.bsd.see_other_uids=0
security.bsd.see_other_gids=0
security.bsd.see_jail_proc=0
kern.randompid=1
# Allow packet filtering in if_bridge(4)
net.link.bridge.pfil_member=1
net.link.bridge.pfil_bridge=1
pkg.conf: |
FreeBSD: {
url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest",
mirror_type: "srv",
signature_type: "fingerprints",
fingerprints: "/usr/share/keys/pkg",
enabled: yes
}
.profile-vmtest.env:
ENTRYPOINT=http://127.0.0.1:8888
TOKEN=<access token>
VM=vmtest
CPU=1
MEM=256M
DISK=10G
VERSION=14.2
SWAP=1G
HOSTNAME=vmtest
DST=provider
Instead of copy and paste the deployment file each time you want to deploy a virtual machine, it is preferable to create several environment (or profile-like) files.
$ overlord -e .profile-vmtest.env apply -f metadata.yml
$ overlord -e .profile-vmtest.env apply -f vm.yml
$ overlord -e .profile-vmtest.env get-info -f vm.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- provider
- vm-only
projects:
vmtest:
state: UNFINISHED
last_log: 2025-04-22_20h19m34s
locked: True
services:
- {'name': 'vm', 'status': 0, 'jail': 'vmtest'}
up:
operation: RUNNING
last_update: 58.85 seconds
job_id: 17
Depending on the type of installation, this may take some time. In the above case we chose to install FreeBSD from its components, so if the server does not have them yet or if it has them but they change remotely (for example: modification time), Overlord will proceed to download them.
$ overlord -e .profile-vmtest.env get-info -f vm.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- provider
- vm-only
projects:
vmtest:
state: DONE
last_log: 2025-04-22_20h19m34s
locked: False
services:
- {'name': 'vm', 'status': 0, 'jail': 'vmtest'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 6 minutes and 10.02 seconds
job_id: 17
restarted: False
$ overlord -e .profile-vmtest.env get-info -f vm.yml -t vm --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- provider
- vm-only
projects:
vmtest:
virtual-machines:
operation: COMPLETED
output: |
md0 created
md0p1 added
md0p2 added
md0p3 added
/dev/md0p3: 9214.0MB (18870272 sectors) block size 32768, fragment size 4096
using 15 cylinder groups of 625.22MB, 20007 blks, 80128 inodes.
with soft updates
super-block backups (for fsck_ffs -b #) at:
192, 1280640, 2561088, 3841536, 5121984, 6402432, 7682880, 8963328, 10243776,
11524224, 12804672, 14085120, 15365568, 16646016, 17926464
Using inode 4 in cg 0 for 75497472 byte journal
bootcode written to md0
partcode written to md0p1
ifconfig_vtnet0: -> inet 192.168.8.2/24
defaultrouter: NO -> 192.168.8.1
fsck_y_enable: NO -> YES
clear_tmp_enable: NO -> YES
dumpdev: NO -> NO
moused_nondefault_enable: YES -> NO
hostname: -> vmtest
[vmtest.appjail] Installing pkg-2.1.0...
[vmtest.appjail] Extracting pkg-2.1.0: .......... done
Updating FreeBSD repository catalogue...
[vmtest.appjail] Fetching meta.conf: . done
[vmtest.appjail] Fetching data.pkg: .......... done
Processing entries: .......... done
FreeBSD repository update completed. 35950 packages processed.
All repositories are up to date.
The following 2 package(s) will be affected (of 0 checked):
New packages to be INSTALLED:
ca_root_nss: 3.108
tailscale: 1.82.5
Number of packages to be installed: 2
The process will require 35 MiB more space.
11 MiB to be downloaded.
[vmtest.appjail] [1/2] Fetching tailscale-1.82.5.pkg: .......... done
[vmtest.appjail] [2/2] Fetching ca_root_nss-3.108.pkg: .......... done
Checking integrity... done (0 conflicting)
[vmtest.appjail] [1/2] Installing ca_root_nss-3.108...
[vmtest.appjail] [1/2] Extracting ca_root_nss-3.108: ....... done
Scanning /usr/share/certs/untrusted for certificates...
Scanning /usr/share/certs/trusted for certificates...
Scanning /usr/local/share/certs for certificates...
[vmtest.appjail] [2/2] Installing tailscale-1.82.5...
[vmtest.appjail] [2/2] Extracting tailscale-1.82.5: ...... done
=====
Message from ca_root_nss-3.108:
--
FreeBSD does not, and can not warrant that the certification authorities
whose certificates are included in this package have in any way been
audited for trustworthiness or RFC 3647 compliance.
Assessment and verification of trust is the complete responsibility of
the system administrator.
This package installs symlinks to support root certificate discovery
for software that either uses other cryptographic libraries than
OpenSSL, or use OpenSSL but do not follow recommended practice.
If you prefer to do this manually, replace the following symlinks with
either an empty file or your site-local certificate bundle.
* /etc/ssl/cert.pem
* /usr/local/etc/ssl/cert.pem
* /usr/local/openssl/cert.pem
tailscaled_enable: -> YES
sshd_enable: NO -> YES
vm_list: -> vmtest
Starting vmtest
* found guest in /vm/vmtest
* booting...
newfs: soft updates journaling set
+ set -o pipefail
+ . /metadata/environment
+ export 'HOSTNAME=vmtest'
+ sysrc -f /mnt/etc/rc.conf 'ifconfig_vtnet0=inet 192.168.8.2/24'
+ sysrc -f /mnt/etc/rc.conf 'defaultrouter=192.168.8.1'
+ sysrc -f /mnt/etc/rc.conf 'fsck_y_enable=YES'
+ sysrc -f /mnt/etc/rc.conf 'clear_tmp_enable=YES'
+ sysrc -f /mnt/etc/rc.conf 'dumpdev=NO'
+ sysrc -f /mnt/etc/rc.conf 'moused_nondefault_enable=NO'
+ sysrc -f /mnt/etc/rc.conf 'hostname=vmtest'
+ [ -f /metadata/resolv.conf ]
+ cp -a /metadata/resolv.conf /mnt/etc/resolv.conf
+ [ -f /metadata/loader.conf ]
+ cp /metadata/loader.conf /mnt/boot/loader.conf
+ [ -f /metadata/zerotier_network ]
+ [ -f /metadata/ts_auth_key ]
+ pkg -c /mnt install -y tailscale
+ head -1 -- /metadata/ts_auth_key
+ ts_auth_key=[REDACTED]
+ echo '/usr/local/bin/tailscale up --accept-dns=false --auth-key="[REDACTED]" && rm -f /etc/rc.local'
+ sysrc -f /mnt/etc/rc.conf 'tailscaled_enable=YES'
+ [ -f /metadata/timezone ]
+ head -1 -- /metadata/timezone
+ timezone=America/Caracas
+ ln -fs /usr/share/zoneinfo/America/Caracas /mnt/etc/localtime
+ [ -f /metadata/sshd_config ]
+ sysrc -f /mnt/etc/rc.conf 'sshd_enable=YES'
+ cp /metadata/sshd_config /mnt/etc/ssh/sshd_config
+ [ -f /metadata/ssh_key ]
+ cp /metadata/ssh_key /mnt/etc/ssh/authorized_keys
+ [ -f /metadata/sysctl.conf ]
+ cp /metadata/sysctl.conf /mnt/etc/sysctl.conf
+ [ -f /metadata/pkg.conf ]
+ mkdir -p /mnt/usr/local/etc/pkg/repos
+ cp /metadata/pkg.conf /mnt/usr/local/etc/pkg/repos/Latest.conf
last_update: 5 minutes and 12.6 seconds
job_id: 17
As I'm using tailscale and the VM above is about to be configured to join my tailnet, after a while it should appear in the node list:
$ tailscale status
...
100.124.236.28 vmtest REDACTED@ freebsd -
$ ssh root@100.124.236.28
The authenticity of host '100.124.236.28 (100.124.236.28)' can't be established.
ED25519 key fingerprint is SHA256:Oc61mU8erpgS2evkwL9WhOOl4Ze94sSNfhImLy3b4UQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '100.124.236.28' (ED25519) to the list of known hosts.
root@vmtest:~ #
Service Discovery
A service is deployed, however depending on how many servers you have in your cluster the service endpoint can be a difficult part to build. You know the port and external interface used and certainly the IP address is easy to get, but it is much easier to use DNS, which is its primary purpose. The service can be deployed on different servers but its endpoint is always the same.
SkyDNS is an older but powerful protocol that, in combination with Etcd, can provide easy service discovery. Overlord can be configured to use both Etcd and SkyDNS. Let's deploy our Etcd cluster.
etcd.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
- r2
- centralita
projectName: etcd-cluster
projectFile: |
options:
- alias:
- ip4_inherit:
services:
etcd:
makejail: 'gh+AppJail-makejails/etcd'
arguments:
- etcd_tag: '14.2-34'
volumes:
- data: etcd-data
start-environment:
- ETCD_NAME: !ENV '${NAME}'
- ETCD_ADVERTISE_CLIENT_URLS: !ENV 'http://${HOSTIP}:2379'
- ETCD_LISTEN_CLIENT_URLS: !ENV 'http://${HOSTIP}:2379'
- ETCD_LISTEN_PEER_URLS: !ENV 'http://${HOSTIP}:2380'
- ETCD_INITIAL_ADVERTISE_PEER_URLS: !ENV 'http://${HOSTIP}:2380'
- ETCD_INITIAL_CLUSTER_TOKEN: 'etcd-demo-cluster'
- ETCD_INITIAL_CLUSTER: !ENV '${CLUSTER}'
- ETCD_INITIAL_CLUSTER_STATE: 'new'
- ETCD_HEARTBEAT_INTERVAL: '5000'
- ETCD_ELECTION_TIMEOUT: '50000'
- ETCD_LOG_LEVEL: 'error'
default_volume_type: '<volumefs>'
volumes:
data:
device: /var/appjail-volumes/etcd-cluster/data
environment:
CLUSTER: 'etcd0=http://100.65.139.52:2380,etcd1=http://100.109.0.125:2380,etcd2=http://100.96.18.2:2380'
labelsEnvironment:
desktop:
NAME: 'etcd0'
HOSTIP: '100.65.139.52'
r2:
NAME: 'etcd1'
HOSTIP: '100.109.0.125'
centralita:
NAME: 'etcd2'
HOSTIP: '100.96.18.2'
Profit!
$ overlord apply -f etcd.yml
$ overlord get-info -f etcd.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
etcd-cluster:
state: DONE
last_log: 2025-04-23_02h28m36s
locked: False
services:
- {'name': 'etcd', 'status': 0, 'jail': 'f094a31c46'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 8 minutes and 11.51 seconds
job_id: 20
restarted: False
labels:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
etcd-cluster:
state: DONE
last_log: 2025-04-23_02h28m37s
locked: False
services:
- {'name': 'etcd', 'status': 0, 'jail': '1ff836df47'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 5 minutes and 37.82 seconds
job_id: 2
restarted: False
labels:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: r2
labels:
- all
- r2
- services
projects:
etcd-cluster:
state: DONE
last_log: 2025-04-23_02h28m38s
locked: False
services:
- {'name': 'etcd', 'status': 0, 'jail': '756ae9d5ca'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 5 minutes and 5.04 seconds
job_id: 1
restarted: False
labels:
error: False
message: None
Once the deployment is done, it is time to configure each Overlord instance with the following parameters.
/usr/local/etc/overlord.yml:
etcd:
100.65.139.52: {}
100.109.0.125: {}
100.96.18.2: {}
Remember to restart the Overlord processes for the changes to take effect.
supervisorctl restart overlord:
The next service we have to deploy is CoreDNS. Thanks to it we can use SkyDNS through the Etcd plugin.
coredns.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
- r2
projectName: dns-server
projectFile: |
options:
- alias:
- ip4_inherit:
services:
coredns:
makejail: !ENV '${OVERLORD_METADATA}/coredns.makejail'
metadata.yml:
kind: metadata
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
- r2
metadata:
Corefile: |
.:53 {
bind tailscale0
log
errors
forward . 208.67.222.222 208.67.220.220
etcd overlord.lan. {
endpoint http://100.65.139.52:2379 http://100.109.0.125:2379 http://100.96.18.2:2379
}
hosts /etc/hosts namespace.lan.
cache 30
}
coredns.hosts: |
100.65.139.52 controller.namespace.lan
100.96.18.2 centralita.namespace.lan
100.127.18.7 fbsd4dev.namespace.lan
100.123.177.93 provider.namespace.lan
100.109.0.125 r2.namespace.lan
172.16.0.3 cicd.namespace.lan
coredns.makejail: |
OPTION start
OPTION overwrite=force
OPTION healthcheck="health_cmd:jail:service coredns status" "recover_cmd:jail:service coredns restart"
INCLUDE gh+DtxdF/efficient-makejail
CMD mkdir -p /usr/local/etc/pkg/repos
COPY ${OVERLORD_METADATA}/coredns.pkg.conf /usr/local/etc/pkg/repos/Latest.conf
PKG coredns
CMD mkdir -p /usr/local/etc/coredns
COPY ${OVERLORD_METADATA}/Corefile /usr/local/etc/coredns/Corefile
COPY ${OVERLORD_METADATA}/coredns.hosts /etc/hosts
SYSRC coredns_enable=YES
SERVICE coredns start
coredns.pkg.conf: |
FreeBSD: {
url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest",
mirror_type: "srv",
signature_type: "fingerprints",
fingerprints: "/usr/share/keys/pkg",
enabled: yes
}
As you can see in the CoreDNS configuration file, the overlord.lan.
zone is assumed but by default Overlord only uses .
which does not make sense for this context, so proceed to configure Overlord with this in mind and deploy CoreDNS.
/usr/local/etc/overlord.yml:
skydns:
zone: 'overlord.lan.'
Note: Remember to restart the Overlord processes for the changes to take effect.
$ overlord apply -f metadata.yml
$ overlord apply -f coredns.yml
$ overlord get-info -f coredns.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
dns-server:
state: DONE
last_log: 2025-04-23_13h32m49s
locked: False
services:
- {'name': 'coredns', 'status': 0, 'jail': '8106aaca6d'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 2 minutes and 30.14 seconds
job_id: 25
restarted: False
labels:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: r2
labels:
- all
- r2
- services
projects:
dns-server:
state: DONE
last_log: 2025-04-23_13h32m54s
locked: False
services:
- {'name': 'coredns', 'status': 0, 'jail': '9516eb48aa'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 3 minutes and 26.9 seconds
job_id: 4
restarted: False
labels:
error: False
message: None
Our Etcd cluster is up and running and our DNS servers are up and running. Clients should be configured to resolve DNS hostnames through those DNS servers, so configure them in their resolv.conf(5)
or similar.
/etc/resolv.conf:
nameserver 100.65.139.52
nameserver 100.109.0.125
Our Frankenstein is alive! So the next step is to deploy a service and test if all parts are working as expected.
homebox.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- centralita
projectName: homebox
projectFile: |
options:
- virtualnet: ':<random> default'
- nat:
services:
homebox:
makejail: gh+AppJail-makejails/homebox
options:
- expose: '8666:7745 ext_if:tailscale0 on_if:tailscale0'
- label: 'overlord.skydns:1'
- label: 'overlord.skydns.group:homebox'
- label: 'overlord.skydns.interface:tailscale0'
volumes:
- data: homebox-data
arguments:
- homebox_tag: 14.2
default_volume_type: '<volumefs>'
volumes:
data:
device: /var/appjail-volumes/homebox/data
So simple. Overlord intercepts the labels that we define in our Director file and based on that it creates the DNS record.
$ overlord apply -f homebox.yml
$ overlord get-info -f homebox.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
homebox:
state: DONE
last_log: 2025-04-23_15h44m38s
locked: False
services:
- {'name': 'homebox', 'status': 0, 'jail': '1f97e32f36'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 4 minutes and 4.1 seconds
job_id: 6
restarted: False
labels:
error: False
message: None
load-balancer:
services:
homebox:
error: False
message: None
skydns:
services:
homebox:
error: False
message: (project:homebox, service:homebox, records:[address:True,ptr:None,srv:None] records has been updated.
Finally, our endpoint is http://homebox.overlord.lan:8666/
Load Balancing
An interesting fact about SkyDNS is that multiple domains are grouped, so if we deploy a service on multiple servers and they use the same group, a DNS request will return three A records (in the case of IPv4), or what amounts to three IPv4 addresses.
hello-http.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- services
projectName: hello-http
projectFile: |
options:
- virtualnet: ':<random> default'
- nat:
services:
darkhttpd:
makejail: 'gh+DtxdF/hello-http-makejail'
options:
- expose: '9128:80 ext_if:tailscale0 on_if:tailscale0'
- label: 'appjail.dns.alt-name:hello-http'
- label: 'overlord.skydns:1'
- label: 'overlord.skydns.group:hello-http'
- label: 'overlord.skydns.interface:tailscale0'
arguments:
- darkhttpd_tag: 14.2
A nice side effect of this is that services are load-balanced in a round-robin fashion, although this is entirely client-dependent, but most modern ones do it.
$ overlord apply -f hello-http.yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_16h26m08s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '7c2225c5fe'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 2 minutes and 43.3 seconds
job_id: 28
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: None
skydns:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, records:[address:True,ptr:None,srv:None] records has been updated.
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
hello-http:
state: DONE
last_log: 2025-04-23_16h26m09s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '3822f65e97'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 2 minutes and 18.56 seconds
job_id: 13
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: None
skydns:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, records:[address:True,ptr:None,srv:None] records has been updated.
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: r2
labels:
- all
- r2
- services
projects:
hello-http:
state: DONE
last_log: 2025-04-23_16h26m10s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '0e0e64eb3c'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 51.17 seconds
job_id: 8
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: None
skydns:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, records:[address:True,ptr:None,srv:None] records has been updated.
$ host -t A hello-http.overlord.lan
hello-http.overlord.lan has address 100.65.139.52
hello-http.overlord.lan has address 100.109.0.125
hello-http.overlord.lan has address 100.96.18.2
$ curl http://hello-http.overlord.lan:9128/
curl http://hello-http.overlord.lan:9128
Hello, world!
UUID: 472ffbdb-9472-4aa2-95ff-39f4bde214df
$ curl http://hello-http.overlord.lan:9128/
Hello, world!
UUID: 7db3b268-87bb-4e81-8be3-e888378fa13b
However, I know that in most cases a more complex configuration is needed. Worse, as noted above this is client-dependent, so it may or may not fit your intent.
Fortunately, Overlord comes with an integration with HAProxy, or more specifically with Data Plane API, so your configuration can be as complex as you need.
haproxy.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
- r2
projectName: load-balancer
projectFile: |
options:
- alias:
- ip4_inherit:
services:
haproxy:
makejail: !ENV '${OVERLORD_METADATA}/haproxy.makejail'
arguments:
- haproxy_tag: 14.2-dataplaneapi
options:
- label: 'overlord.skydns:1'
- label: 'overlord.skydns.group:revproxy'
- label: 'overlord.skydns.interface:tailscale0'
metadata.yml:
kind: metadata
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
- r2
metadata:
haproxy.makejail: |
ARG haproxy_tag=13.5
ARG haproxy_ajspec=gh+AppJail-makejails/haproxy
OPTION start
OPTION overwrite=force
OPTION copydir=${OVERLORD_METADATA}
OPTION file=/haproxy.conf
FROM --entrypoint "${haproxy_ajspec}" haproxy:${haproxy_tag}
INCLUDE gh+DtxdF/efficient-makejail
SYSRC haproxy_enable=YES
SYSRC haproxy_config=/haproxy.conf
SERVICE haproxy start
STOP
STAGE start
WORKDIR /dataplaneapi
RUN daemon \
-r \
-t "Data Plane API" \
-P .master \
-p .pid \
-o .log \
./dataplaneapi \
-f /usr/local/etc/dataplaneapi.yml \
--host=0.0.0.0 \
--port=5555 \
--spoe-dir=/usr/local/etc/haproxy/spoe \
--haproxy-bin=/usr/local/sbin/haproxy \
--reload-cmd="service haproxy reload" \
--restart-cmd="service haproxy restart" \
--status-cmd="service haproxy status" \
--maps-dir=/usr/local/etc/haproxy/maps \
--config-file=/haproxy.conf \
--ssl-certs-dir=/usr/local/etc/haproxy/ssl \
--general-storage-dir=/usr/local/etc/haproxy/general \
--dataplane-storage-dir=/usr/local/etc/haproxy/dataplane \
--log-to=file \
--userlist=dataplaneapi
haproxy.conf: |
userlist dataplaneapi
user admin insecure-password cuwBvS5XMphtCNuC
global
daemon
log 127.0.0.1:514 local0
log-tag HAProxy
defaults
mode http
log global
option httplog
timeout client 30s
timeout server 50s
timeout connect 10s
timeout http-request 10s
frontend web
bind :80
default_backend web
backend web
option httpchk HEAD /
balance roundrobin
We are about to deploy HAProxy / Data Plane API on two servers. The reason for doing this is to avoid SPOF (single point of failure), at least if one instance of HAProxy / Data Plane API goes down at any time, the other will rescue us. However, Overlord can only point to one instance of Data Plane API, so if we use two (as below) servers, we need to specify one instance on one and a different instance on the other. As you can see in the Director file, we have used SkyDNS, so that clients can use the revproxy.overlord.lan
domain instead of each individual IP address, with the advantage that even if one instance of HAProxy / Data Plane API is down, we have the other and the client can make requests to the other.
$ overlord apply -f metadata.yml
$ overlord apply -f haproxy.yml
$ overlord get-info -f haproxy.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
load-balancer:
state: DONE
last_log: 2025-04-23_17h04m01s
locked: False
services:
- {'name': 'haproxy', 'status': 0, 'jail': '8d92fc6d2d'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 2 minutes and 20.12 seconds
job_id: 30
restarted: False
labels:
error: False
message: None
load-balancer:
services:
haproxy:
error: False
message: None
skydns:
services:
haproxy:
error: False
message: (project:load-balancer, service:haproxy, records:[address:True,ptr:None,srv:None] records has been updated.
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: r2
labels:
- all
- r2
- services
projects:
load-balancer:
state: DONE
last_log: 2025-04-23_17h04m02s
locked: False
services:
- {'name': 'haproxy', 'status': 0, 'jail': '05c589c8a1'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 2 minutes and 53.27 seconds
job_id: 10
restarted: False
labels:
error: False
message: None
load-balancer:
services:
haproxy:
error: False
message: None
skydns:
services:
haproxy:
error: False
message: (project:load-balancer, service:haproxy, records:[address:True,ptr:None,srv:None] records has been updated.
/usr/local/etc/overlord.yml (centralita):
dataplaneapi:
auth:
username: 'admin'
password: 'cuwBvS5XMphtCNuC'
entrypoint: 'http://100.65.139.52:5555'
/usr/local/etc/overlord.yml (provider):
dataplaneapi:
auth:
username: 'admin'
password: 'cuwBvS5XMphtCNuC'
entrypoint: 'http://100.109.0.125:5555'
hello-http.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- centralita
- provider
projectName: hello-http
projectFile: |
options:
- virtualnet: ':<random> default'
- nat:
services:
darkhttpd:
makejail: 'gh+DtxdF/hello-http-makejail'
options:
- expose: '9128:80 ext_if:tailscale0 on_if:tailscale0'
- label: 'overlord.load-balancer:1'
- label: 'overlord.load-balancer.backend:web'
- label: 'overlord.load-balancer.interface:tailscale0'
- label: 'overlord.load-balancer.interface.port:9128'
- label: 'overlord.load-balancer.set.check:"enabled"'
arguments:
- darkhttpd_tag: 14.2
Profit!
$ overlord apply -f hello-http.yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: provider
labels:
- all
- provider
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_17h57m16s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '79f16243de'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 1 minute and 22.1 seconds
job_id: 1
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:8fcf5d67-12df-4fcd-a2e3-1f5f18fe1844, commit:1) server has been successfully added.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
hello-http:
state: DONE
last_log: 2025-04-23_17h57m16s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '52dfa071cb'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 1 minute and 19.53 seconds
job_id: 15
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:0fa886c8-68a6-4716-aa6b-824aa3e776ad, commit:1) server has been successfully added.
skydns:
services:
darkhttpd:
error: False
message: None
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: 8579af73-7d11-40b3-8444-6dac62e34b8e
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: e463b1d5-13eb-4f04-9b0a-caf4339a8058
Horizontal Autoscaling
Even when there are hundreds of servers, deploying projects is an easy task, however the problem with this approach is that we are wasting resources and the clients probably only use less than 5% of the resources of our cluster, or on the contrary, you can deploy your project on a few servers which you think is enough until you realize at some point that this is not enough, even worse, some servers can be down at any time for any reason. This is what Overlord autoscaling can solve.
hello-http.yml:
kind: directorProject
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- desktop
projectName: hello-http
projectFile: |
options:
- virtualnet: ':<random> default'
- nat:
services:
darkhttpd:
makejail: 'gh+DtxdF/hello-http-makejail'
options:
- expose: '9128:80 ext_if:tailscale0 on_if:tailscale0'
- label: 'overlord.load-balancer:1'
- label: 'overlord.load-balancer.backend:web'
- label: 'overlord.load-balancer.interface:tailscale0'
- label: 'overlord.load-balancer.interface.port:9128'
- label: 'overlord.load-balancer.set.check:"enabled"'
arguments:
- darkhttpd_tag: 14.2
autoScale:
replicas:
min: 3
labels:
- services
- provider
As you have probably noticed, we have specified two types of labels. This is the subtle difference between autoscaling and non-autoscaling deployments. Unlike non-autoscaling deployments, the labels in deployIn.labels
are for using matching servers for autoscaling and monitoring, or in other words, the servers that match the specified labels (in this case desktop
) are responsible for deployment, monitoring and, if necessary, redeployment. On the other side, the servers matching the labels in autoScale.labels
(in this case services
and provider
) are for deploying the project as non-autoscaling deployments. We have specified that the project will have at least three replicas. There are other things we can specify like rctl(8)
rules, but for simplicity this is sufficient.
$ overlord apply -f hello-http.yml
$ overlord get-info -f hello-http.yml -t projects --filter-per-project --use-autoscale-labels
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_19h36m17s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '0524bcf91b'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 58.24 seconds
job_id: 31
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:0d67d160-61af-4810-b277-5fb9e20da8eb, code:202, transaction_id:baa5b939-f724-4bd3-9d65-2ef769def3f5, commit:1) server has been successfully added.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: provider
labels:
- all
- provider
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_20h00m11s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '2c2d22d2a5'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 4 minutes and 46.3 seconds
job_id: 6
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:6792e6fe-a
778-44a7-b23a-1b2c23fe5904, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
hello-http:
state: DONE
last_log: 2025-04-23_20h04m25s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': 'a6549318ce'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 33.34 seconds
job_id: 21
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:00e632ce-c215-4784-9e61-9507d914ba6a, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
$ overlord get-info -f hello-http.yml -t autoscale --filter-per-project
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
hello-http:
autoScale:
last_update: 7.65 seconds
operation: COMPLETED
output:
message: None
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: 08951a86-2aef-4e85-9bfc-7fe68b5cc62d
$ curl http://revproxy.overlord.lan
Hello, world!
UUID: 5a06a89d-6109-438e-bc04-1ef739473994
Suppose the service in provider
is down for any reason.
$ overlord get-info -f hello-http.yml -t projects --filter-per-project --use-autoscale-labels
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_19h36m17s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '0524bcf91b'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 13 minutes and 37.64 seconds
job_id: 32
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:0d67d160-61af-4810-b277-5fb9e20da8eb, code:202, transaction_id:a2ba93d7-6ce6-4a36-aab2-09be13a00c17, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: provider
labels:
- all
- provider
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_20h11m58s
locked: False
services:
- {'name': 'darkhttpd', 'status': 66, 'jail': '2c2d22d2a5'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 1 minute and 13.9 seconds
job_id: 7
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:b19e7997-871c-4293-a8a9-51ce03f2bbaa, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
hello-http:
state: DONE
last_log: 2025-04-23_20h04m25s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': 'a6549318ce'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 8 minutes and 29.3 seconds
job_id: 21
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:00e632ce-c215-4784-9e61-9507d914ba6a, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
Without any intervention, let's see the magic.
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: None
labels:
- all
- desktop
- services
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_19h36m17s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '0524bcf91b'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 14 minutes and 30.7 seconds
job_id: 32
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:0d67d160-61af-4810-b277-5fb9e20da8eb, code:202, transaction_id:a2ba93d7-6ce6-4a36-aab2-09be13a00c17, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: provider
labels:
- all
- provider
- vm-only
projects:
hello-http:
state: DONE
last_log: 2025-04-23_20h13m37s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': '2c2d22d2a5'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 27.47 seconds
job_id: 8
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:fa8f94b1-6b2b-4cb4-a808-e9da46014c86, code:202, transaction_id:98ca3a6a-65e4-450b-a4c3-4f135e36be37, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
hello-http:
state: DONE
last_log: 2025-04-23_20h04m25s
locked: False
services:
- {'name': 'darkhttpd', 'status': 0, 'jail': 'a6549318ce'}
up:
operation: COMPLETED
output:
rc: 0
stdout: {'errlevel': 0, 'message': None, 'failed': []}
last_update: 9 minutes and 22.37 seconds
job_id: 21
restarted: False
labels:
error: False
message: None
load-balancer:
services:
darkhttpd:
error: False
message: (project:hello-http, service:darkhttpd, backend:web, serverid:f4b9e170-67bb-403e-88da-112c55b45fce, code:202, transaction_id:00e632ce-c215-4784-9e61-9507d914ba6a, commit:1) server has been successfully updated.
skydns:
services:
darkhttpd:
error: False
message: None
The service is alive again.
Info, Metrics, and more...
Thanks to AppJail, a lot of information can be obtained from jails. Overlord has a special deployment called readOnly
that can be perfectly combined with the get-info
command.
info.yml:
kind: readOnly
datacenters:
main:
entrypoint: !ENV '${ENTRYPOINT}'
access_token: !ENV '${TOKEN}'
deployIn:
labels:
- all
$ overlord get-info -f info.yml -t projects --filter adguardhome
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
projects:
adguardhome:
state: DONE
last_log: 2025-04-07_17h32m40s
locked: False
services:
- {'name': 'server', 'status': 0, 'jail': '2a67806954'}
$ overlord get-info -f info.yml -t jails --filter 2a67806954
datacenter: http://127.0.0.1:8888
entrypoint: main
chain: centralita
labels:
- all
- centralita
- services
jails:
2a67806954:
stats:
cputime: 91
datasize: 8400896 (8.01 MiB)
stacksize: 0 (0 bytes)
coredumpsize: 0 (0 bytes)
memoryuse: 75104256 (71.62 MiB)
memorylocked: 0 (0 bytes)
maxproc: 4
openfiles: 296
vmemoryuse: 1367982080 (1.27 GiB)
pseudoterminals: 0
swapuse: 0 (0 bytes)
nthr: 13
msgqqueued: 0
msgqsize: 0
nmsgq: 0
nsem: 0
nsemop: 0
nshm: 0
shmsize: 0 (0 bytes)
wallclock: 363548
pcpu: 0
readbps: 0 (0 bytes)
writebps: 0 (0 bytes)
readiops: 0
writeiops: 0
info:
name: 2a67806954
network_ip4: 10.0.0.3
ports: 53/tcp,53/udp,53/tcp,53/udp
status: UP
type: thin
version: 14.2-RELEASE
cpuset: 0, 1
expose:
- {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'udp', 'ext_if': 'tailscale0', 'on_if': 'tailscale0', 'nro': '3'}
- {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'tcp', 'ext_if': 'jext', 'on_if': 'jext', 'nro': '0'}
- {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'tcp', 'ext_if': 'tailscale0', 'on_if': 'tailscale0', 'nro': '2'}
- {'enabled': '1', 'name': None, 'network_name': 'ajnet', 'hport': '53', 'jport': '53', 'protocol': 'udp', 'ext_if': 'jext', 'on_if': 'jext', 'nro': '1'}
fstab:
- {'enabled': '1', 'name': None, 'device': '/var/appjail-volumes/adguardhome/db', 'mountpoint': 'adguardhome-db', 'type': '<volumefs>', 'options': 'rw', 'dump': '0', 'pass': None, 'nro': '0'}
labels:
- {'value': '1', 'name': 'overlord.skydns'}
- {'value': 'adguardhome', 'name': 'appjail.dns.alt-name'}
- {'value': 'tailscale0', 'name': 'overlord.skydns.interface'}
- {'value': 'adguardhome', 'name': 'overlord.skydns.group'}
nat:
- {'rule': 'nat on "jext" from 10.0.0.3 to any -> ("jext:0")', 'network': 'ajnet'}
volumes:
- {'mountpoint': 'usr/local/etc/AdGuardHome.yaml', 'type': '<pseudofs>', 'uid': None, 'gid': None, 'perm': '644', 'name': 'adguardhome-conf'}
- {'mountpoint': '/var/db/adguardhome', 'type': '<pseudofs>', 'uid': None, 'gid': None, 'perm': '750', 'name': 'adguardhome-db'}
Future Work
There are more things Overlord can do for you than this document provides, see the Wiki for more examples.
Overlord is a recent project, there is a lot of room for improvement and future features will be added to improve its usability. If you would like to support the project, please consider donating.