Quick start: a single node
You're going to take one ordinary machine (a mini PC, an old laptop or a Raspberry Pi 4/5) from a bare Linux install to a real app on a real HTTPS address. It takes about 30 minutes. This page explains why each step exists, what you should see after it, and what to do when you don't see it.
One honest warning before you start: with a single node, if the machine dies your apps are down until it boots again. Nothing can take over. That is fine for learning and for many home setups, and quick start 2 shows how to fix it by adding two more machines.
What you'll build
By the end you'll have:
- the Clouduler daemon running as a systemd service,
- a one-node cluster called
homelab, - the web UI open in your browser,
- Vaultwarden (a password manager) running, reachable on your LAN, then on a real domain with a real certificate.
Tell the page about your machine
Everything below is written with example values. Fill this in once and every command on this page (and on quick start 2) uses yours. Your answers stay in your browser.
What you need
- A 64-bit machine running Linux with systemd: Debian 13, Raspberry Pi OS based on Debian 13 (trixie), or Arch Linux.
uname -mmust printx86_64oraarch64, because those are the only two binaries the project releases. - Root access (the commands use
sudo) and a fixed IP address on your LAN. Give the machine a DHCP reservation in your router if it does not have one, because the cluster remembers this address. - A few GB of free disk for Docker images and for the drive directory.
- Optional, for the last part: a domain on Cloudflare and ports 80 and 443 forwarded to this machine.
Don't use it. The release binary needs glibc 2.39 or newer (checked on the aarch64 build), so it runs on Debian 13 but not on Debian 12, which ships glibc 2.36.
1. Prepare the machine
Connect to the machine and install what Clouduler leans on. Docker runs the containers. iproute2, iptables and ethtool are used to build the overlay networks between stacks. restic is only needed for backups, but install it now so you don't forget.
ssh {{user}}@{{ip}}
[debian,raspbian] sudo apt update
[debian,raspbian] sudo apt install -y docker.io iproute2 iptables ethtool restic
[arch] sudo pacman -S --needed docker iproute2 iptables ethtool restic
sudo systemctl enable --now docker
{{user}}{{ip}} (fill in the setup form above)docker.io is Debian's own Docker package. If you use Docker's vendor repository instead, install docker-ce rather than docker.io.
Why enable --now? Clouduler starts after Docker and needs its socket. If Docker is not up at boot, the daemon waits up to two minutes for it and keeps retrying, but nothing works until Docker answers.
Now check that Docker is really up.
docker info | grep 'Server Version'
What you should see: a version number.
Cannot connect to the Docker daemonDocker is not running. Run sudo systemctl status docker to see why. On a Raspberry Pi, the usual cause of a Docker that starts but shows no memory limits is a missing kernel flag: add cgroup_enable=memory cgroup_memory=1 to /boot/firmware/cmdline.txt and reboot.
Stacks that have their own network use a VXLAN device (on a single node, public networks do not), so load that kernel module and check it. If you later serve a drive over NFS, the nfsv4 module is needed too.
sudo modprobe vxlan
lsmod | grep -E 'vxlan|bridge'
Clouduler never loads this module for you and does not persist it across reboots. To persist it, put vxlan on a line in /etc/modules-load.d/clouduler.conf.
Finally, the folder that will back your first replicated drive. Call the drive fast. A drive is a named pool of storage that Clouduler can copy between nodes (see Drives and volumes). On one node there is nothing to copy to yet, but declaring it now means your app's data already lives where it will be replicated later.
sudo mkdir -p /srv/clouduler/fast
Pick a path on a disk with room. It does not have to be this one.
2. Install Clouduler
Clouduler is one static-ish binary that is both the CLI and the daemon. Find out which one you need.
echo "$(uname -m)-unknown-linux-gnu"
What you should see: x86_64-unknown-linux-gnu or aarch64-unknown-linux-gnu. Anything else means there is no prebuilt binary for this machine and you would have to build from source (see the repository README).
You have two ways to get it. You can compile Clouduler yourself (see the repository README), or you can download the prebuilt binary from the latest release. Take the prebuilt one: the release asset is the bare binary called clouduler- followed by that string, with no archive to unpack. Download it and copy it to the node. Then install it.
# from your laptop, in the folder where you downloaded it
scp clouduler-x86_64-unknown-linux-gnu {{user}}@{{ip}}:/tmp/clouduler
# on the node
sudo install -m 0755 /tmp/clouduler /usr/local/bin/clouduler
clouduler --version
{{user}}{{ip}} (fill in the setup form above)What you should see: clouduler and a version number. clouduler --help lists subcommands like init, daemon, stack, status and check.
init copies the running binary to /usr/bin/clouduler, and that is the copy the service runs. The one in /usr/local/bin stays. When you upgrade later, replace both (or let the cluster do it, see Updates). do not re-run init for that: it refuses to run on a machine that already has a cluster.yaml unless you pass --force, which starts over. which -a clouduler shows both.
3. Initialise the cluster
init is the one command that turns this machine into the first node of a new cluster. It writes /etc/clouduler/cluster.yaml, creates folders, installs the systemd unit and starts it. Give it every answer on the command line so you can copy it into your notes.
sudo clouduler init --yes \
--cluster-name {{cluster}} \
--subnet {{subnet}} \
--internal-domain {{domain}} \
--web-address cloud \
--ip {{ip}} \
--node {{hostname}} \
--drive fast=/srv/clouduler/fast
{{cluster}}{{subnet}}{{domain}}{{ip}}{{hostname}} (fill in the setup form above)What each flag is for:
| Flag | Why |
|---|---|
--yes | Take the default for every question instead of asking. |
--subnet | The internal network. Every node's address must be inside it, and cluster traffic runs over it, so use a LAN you trust. |
--internal-domain | The domain under which the web UI and LAN-only apps get names. It is not published to public DNS. |
--web-address cloud | The UI hostname. With the domain above it becomes cloud.lan.example.com. |
--ip, --node | This machine's address and name. Both are detected if omitted, but being explicit avoids surprises. |
--drive fast=... | Declares the drive fast and says where it lives on this node. Without any --drive, volumes cannot be used. |
Leaving out --internal-domain makes the UI address default to a placeholder (cloud.example.com), and giving a bare --web-address cloud with no domain is refused.
What you should see: a block of progress lines, ending with something like:
wrote /etc/clouduler/cluster.yaml
...
the daemon is running
== done
atlas is a manager in cluster homelab at 192.168.1.10
the web ui is at https://cloud.lan.example.com:8443
It also prints a reminder that the daemon will generate the cluster key at /etc/clouduler/secrets.key. Remember that key: it is the one thing you must back up, and you will back it up in a minute.
ethtool or restic is not on PATHIt is only a warning. Install the tool before you create stacks with their own network (ethtool) or set up backups (restic).
this machine is not running systemdNo unit was installed. You would have to start the daemon by hand, which this page does not cover.
4. Start the service and check it
init already did systemctl enable --now. Confirm it.
sudo systemctl status clouduler.service --no-pager
What you should see: Active: active (running).
failed for another reasonRead the log with sudo journalctl -u clouduler -b --no-pager | tail -50. The most common causes are Docker not running and another program already holding port 80 or 443 (sudo ss -ltnp | grep -E ':(80|443) '). If something else owns those ports, the proxy silently cannot bind them.
Then look at the log for the three lines that tell you it came up properly.
sudo journalctl -u clouduler -b --no-pager | grep -E 'starting|web ui listening|generated a new cluster key'
You should see starting, a warning that a new cluster key was generated, and web ui listening. A warning that the UI is served "with a self signed certificate" is normal for now.
Back up the cluster key right now
Every secret Clouduler stores is encrypted with /etc/clouduler/secrets.key. If the machine's disk dies and you don't have a copy, those secrets are gone for good. So before anything else:
sudo cp -a /etc/clouduler/secrets.key /root/clouduler-secrets.key.backup
sudo chmod 600 /root/clouduler-secrets.key.backup
Then copy that file off this machine, to a password manager attachment or an encrypted USB stick. Not a chat, not an email.
5. Open the web UI and sign in
Get the first API key
On its first start with no keys at all, the daemon mints one admin API key and prints it in the log once. It stores only a hash of it, so if you lose it you cannot recover it, and there is no CLI command that creates a new one.
sudo journalctl -u clouduler --no-pager | grep -o 'clk_[A-Za-z0-9]*_[A-Za-z0-9]*'
What you should see: one line like clk_xxxxxxxxxxxx_yyyyyyyy.... copy it into your password manager immediately.
The key is only logged when the cluster has a leader and no keys exist. Wait ten seconds and try again, and make sure you are searching the whole journal (no -b) in case the node rebooted. If the key was already logged and you did not save it, the only way back in is a second sign-in method (OIDC) that you set up earlier; the emergency tools do not create or restore API keys. So save the key now. A lost key with no OIDC configured has no documented recovery.
Make the name resolve
The UI lives at https://cloud.lan.example.com:8443. That name has to resolve on your laptop. Without a LAN DNS server, add a line to /etc/hosts on your laptop:
{{ip}} cloud.{{domain}}
{{ip}}{{domain}} (fill in the setup form above)Use the name rather than the raw IP (https://192.168.1.10:8443) to sign in. If you later set up single sign-on, its callback is fixed to the dashboard name and the login cookie is only valid for the host you typed, so a login started on the IP fails.
Sign in
Open https://cloud.lan.example.com:8443. Your browser warns about the certificate because Clouduler made a self-signed one. That is expected, and accept it for this host; a real certificate comes in step 8.
The login page asks for an API key under "or use an API key". Paste the clk_... key into API key and press Sign in with API key.
What you should see: the Overview with one node and "No stacks yet".
The key was copied with a missing character or a space. Copy it again from the log. The session lasts 12 hours by default.
Check that the name resolves (ping cloud.lan.example.com) and that port 8443 is reachable (curl -k https://192.168.1.10:8443). A host firewall on the node must allow 8443/tcp.
Before you deploy anything, run a health check on the node.
sudo clouduler status
What you should see: one node, atlas, as a manager, and a last line all clear: no unhealthy instances or cluster faults.
6. Deploy a first app: Vaultwarden
You're going to use Vaultwarden, a small Bitwarden-compatible password manager. It is a single container, and it uses a volume, so it shows the one thing that makes Clouduler different from plain Docker: where its data lives is a cluster decision, not a path on one disk.
This is the stack. Read it once; the next page, Stacks, explains every key.
networks:
internal: {}
services:
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
networks: [internal]
volumes:
- fast/vaultwarden:/data
x-clouduler:
proxy:
- host: vault
network: internal
port: 80
networks: internalattaches the container to the LAN-only network thatinitcreated.fast/vaultwarden:/datameans "the volumevaultwardenon drivefast, mounted at/data". That is the folder you created in step 1.- The
proxyroute says "publish container port 80 asvaulton the internal network". The name gets the network's domain appended, so it becomesvault.lan.example.com.
- Web UI
- CLI / config
- In the sidebar open Stacks and click Deploy stack.
- Type
vaultwardenas the stack name and paste the YAML above into the spec editor. - On the right, watch the panels. You want the banner Spec is valid, and under Where it can run you want to see
atlaslisted. - Click the Deploy vaultwarden button (it carries the stack name, and stays disabled until the spec is valid). you land on the stack page, where the container goes from "pending placement" to "running" while Docker pulls the image.
Save the YAML as vaultwarden.yaml on the node and deploy it. The stack takes the file's name.
sudo clouduler stack deploy vaultwarden.yaml
sudo clouduler stack unhealthy --placement
sudo clouduler status
stack unhealthy lists instances that are not running yet; once it prints nothing for vaultwarden, the container is up.
Read the sentence after it. It names the step that failed. The usual first-time causes are a network or drive name in the YAML that does not exist in your cluster (the editor autocompletes the real ones), or a typo in the volume.
Open the stack's Placement tab. It lists each rule and which one failed for atlas.
7. Reach it on the LAN
The proxy routes by hostname, so your laptop needs to resolve vault.lan.example.com to the node, the same way you did for cloud. Add one more /etc/hosts line:
{{ip}} cloud.{{domain}} vault.{{domain}}
{{ip}}{{domain}} (fill in the setup form above)curl -k -I https://vault.{{domain}}/
{{domain}} (fill in the setup form above)What you should see: an HTTP/2 200 (or similar). -k is there because the proxy serves a self-signed certificate until a real one exists.
Routes on a subnetted network only answer clients whose address is inside the subnet. Your laptop must be in 192.168.1.0/24. Anything outside, including a VPN, gets a 403. To publish something to the outside world, use a public network (next step). See Proxy and TLS.
Check that the proxy owns ports 80 and 443 (sudo ss -ltnp | grep -E ':(80|443) ' should show clouduler) and that the host firewall allows them.
Vaultwarden works in a browser over a self-signed certificate if you accept the warning, but its phone apps and browser extensions will refuse it. That is the reason for the next step.
8. Give it a real domain with HTTPS
To get a certificate that browsers trust, Clouduler needs to prove to Let's Encrypt that it controls the name. It does that through Cloudflare DNS, and it also creates the DNS records for you. This part needs a domain on Cloudflare and port 443 reachable from the internet. The certificate check goes through DNS, so port 80 is not needed for it. This page uses example.com; use yours.
- Create a Cloudflare API token with permission to edit DNS on your zone, following Cloudflare API token. The token is stored sealed inside the cluster, never in a file.
- In the web UI open Settings, then Network, and on the Cloudflare DNS page turn on Manage DNS with Cloudflare, set DNS zones to
example.com, and use Save token to store the token. - On the Reverse proxy page turn on Issue certificates automatically, set Contact email, and while you're testing set ACME directory URL to Let's Encrypt's staging address
https://acme-staging-v02.api.letsencrypt.org/directory. - On the Networks page add a network named
wanof type public with the domainexample.com. - On the Nodes page give
atlasits public IPv4 address forwan. A service on a public network can only be placed on a node that has a public address for it.
If you leave ACME directory URL empty, Clouduler uses Let's Encrypt production, which rate-limits failures. Test on staging, and be aware that the ACME account Clouduler creates is remembered together with the directory it was created against. Clearing the field later does not re-point that account, so only use staging when you are ready to wipe the account file (acme-account.json in the data directory) before switching.
Settings pages that affect networking ask you to confirm the change within a minute, or they roll back. That is a safety net so you can't lock yourself out. See Live config.
Then point the stack at the new network. In the Deploy page for vaultwarden (or in the file) change internal to wan in both places:
networks:
wan: {}
services:
vaultwarden:
networks: [wan]
x-clouduler:
proxy:
- host: vault
network: wan
port: 80
and redeploy (Deploy vaultwarden in the UI, or sudo clouduler stack deploy vaultwarden.yaml). The route is now vault.example.com.
What you should see: after a short wait the stack shows a certificate and DNS record in the DNS page, and https://vault.example.com opens with a (staging) certificate. Getting the trusted certificate afterwards means clearing the ACME URL and also removing the stored account, as described in the warning above; that switch has not been tested.
sudo clouduler status lists certificate and DNS problems. The common causes are a token without DNS edit rights, a zone that is not listed under DNS zones, or port 443 not reaching the node. See DNS publishing.
A route on a public network is not restricted by source address. Anyone on the internet can reach Vaultwarden's login page. Pick a strong master password, and read the Vaultwarden notes before you rely on it.
Open https://vault.example.com, create your account, and you have a password manager that you run.
9. Check that it's healthy
sudo clouduler status
sudo clouduler logs vaultwarden -f
status should end with all clear: no unhealthy instances or cluster faults and report stacks 1 deployed, 1/1 instances assigned to run. logs streams the container's output; Ctrl-C leaves it. In the UI, the Overview shows the same, and the Nodes page shows atlas as healthy.
The files that matter, if you ever need them: the config is /etc/clouduler/cluster.yaml (edit it in the UI, not by hand, because edits are replicated), the cluster key is /etc/clouduler/secrets.key, the data directory is /var/lib/clouduler, and the drive is /srv/clouduler/fast.
10. Turn on automatic updates
Point the cluster at the official releases so it knows when a new version is out. In the UI, open Settings, then Updates, enter https://github.com/LiteApplication/clouduler as the Repository and click Save release. This can be set once from the UI. Leave Automatic checks on.
Or put it in cluster.yaml (the same keys the UI writes):
self_update:
release_url: https://github.com/LiteApplication/clouduler
check_updates: true
check_interval: 24h
Clouduler then looks for a newer release every day and shows it in Settings, Updates. Nothing is installed by itself: you click Start update when you are ready, and the nodes restart one at a time. See Updates.
What you just learned
- One binary, one command (
init), and a node is a cluster. - A stack is a YAML file. You deploy it from the UI or
clouduler stack deploy. - A network decides who can reach an app and under which domain:
internalfor the LAN, a public network for the internet. - A drive is where volumes live, and Clouduler is the thing that decides where and how they are copied.
- The cluster key and the first API key are the two things you must keep.
Next steps
- Add two more machines and watch an app survive a node failure.
- Set up Backups, because replication is not a backup.
- Learn how placement decides where things run and how proxy and TLS work.
- Skim the stack spec reference.