Skip to main content

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.

Your setup

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 -m must print x86_64 or aarch64, 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.
Raspberry Pi OS on Debian 12 (bookworm)

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.

On the node
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
Not set yet: {{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.

If you see Cannot connect to the Docker daemon

Docker 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
Not set yet: {{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.

Two copies of the binary

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.

Create the cluster
sudo clouduler init --yes \
--cluster-name {{cluster}} \
--subnet {{subnet}} \
--internal-domain {{domain}} \
--web-address cloud \
--ip {{ip}} \
--node {{hostname}} \
--drive fast=/srv/clouduler/fast
Not set yet: {{cluster}}{{subnet}}{{domain}}{{ip}}{{hostname}} (fill in the setup form above)

What each flag is for:

FlagWhy
--yesTake the default for every question instead of asking.
--subnetThe internal network. Every node's address must be inside it, and cluster traffic runs over it, so use a LAN you trust.
--internal-domainThe domain under which the web UI and LAN-only apps get names. It is not published to public DNS.
--web-address cloudThe UI hostname. With the domain above it becomes cloud.lan.example.com.
--ip, --nodeThis 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.

If you see a warning that ethtool or restic is not on PATH

It is only a warning. Install the tool before you create stacks with their own network (ethtool) or set up backups (restic).

If you see this machine is not running systemd

No 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).

If the service is failed for another reason

Read 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.

If you see nothing

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:

/etc/hosts on your laptop
{{ip}} cloud.{{domain}}
Not set yet: {{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.

/
The Overview of a brand-new one-node cluster.

What you should see: the Overview with one node and "No stacks yet".

If you see "That key was not accepted"

The key was copied with a missing character or a space. Copy it again from the log. The session lasts 12 hours by default.

If the page does not load at all

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.

vaultwarden.yaml
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: internal attaches the container to the LAN-only network that init created.
  • fast/vaultwarden:/data means "the volume vaultwarden on drive fast, mounted at /data". That is the folder you created in step 1.
  • The proxy route says "publish container port 80 as vault on the internal network". The name gets the network's domain appended, so it becomes vault.lan.example.com.
  1. In the sidebar open Stacks and click Deploy stack.
  2. Type vaultwarden as the stack name and paste the YAML above into the spec editor.
  3. On the right, watch the panels. You want the banner Spec is valid, and under Where it can run you want to see atlas listed.
  4. 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.
If you see a red "Not deployed" banner

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.

If the container stays in "pending placement"

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:

/etc/hosts on your laptop
{{ip}} cloud.{{domain}} vault.{{domain}}
Not set yet: {{ip}}{{domain}} (fill in the setup form above)
curl -k -I https://vault.{{domain}}/
Not set yet: {{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.

If you see 403

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.

If the connection is refused or times out

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.

  1. 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.
  2. 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.
  3. 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.
  4. On the Networks page add a network named wan of type public with the domain example.com.
  5. On the Nodes page give atlas its public IPv4 address for wan. A service on a public network can only be placed on a node that has a public address for it.
Set the staging URL first

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:

vaultwarden.yaml (changed lines)
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.

If the certificate stays pending or shows an error

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.

Public means public

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):

cluster.yaml
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: internal for 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​