Skip to main content

Systemd Deployment — External Mosquitto

End-to-end recipe for running HomeCore as a native systemd service, paired with an external Mosquitto broker that enforces per-topic ACLs (the embedded rumqttd does not). If you're new to the trade-off between the embedded broker and an external one, read the broker guide first.

If you prefer Docker, the compose variant is in the core repo at core/docker/docker-compose.external-broker.yml and the flow is equivalent — same generated files, just different packaging.

This guide assumes a Debian/Ubuntu-style distribution with systemd and apt; Red Hat / Arch / NixOS users will need to adapt package + path conventions.


Layout

By the end you'll have:

/etc/homecore/
├── homecore.toml # HomeCore config (broker.external_url set)

/etc/mosquitto/
├── mosquitto.conf # generated by hc-cli
├── acl # generated aclfile
├── passwd # hashed from passwd.setup.sh
└── mosquitto.conf.d/ # standard drop-in dir (unused here)

/var/lib/homecore/ # state.redb, history.db, audit.db,
# jwt_secret (0600)

/var/log/homecore/ # rolling tracing log (if configured)

/etc/systemd/system/
├── homecore.service # HomeCore unit
└── mosquitto.service.d/override.conf # optional: tighten Mosquitto

1. Install Mosquitto

sudo apt install -y mosquitto mosquitto-clients
sudo systemctl stop mosquitto

We stop it immediately — the default config is anonymous-allowed, which we'll replace.

2. Install HomeCore

If you build from source:

cd homeCore/core
cargo build --release
sudo install -m 0755 target/release/homecore /usr/local/bin/
sudo install -m 0755 target/release/hc-cli /usr/local/bin/

If you use a prebuilt tarball, extract it under /opt/homecore and copy the binaries the same way.

Create the service user and directories:

sudo useradd --system --home-dir /var/lib/homecore --shell /usr/sbin/nologin homecore
sudo install -d -o homecore -g homecore -m 0750 /var/lib/homecore
sudo install -d -o homecore -g homecore -m 0750 /var/log/homecore
sudo install -d -o root -g homecore -m 0750 /etc/homecore

3. Author homecore.toml

Drop your config at /etc/homecore/homecore.toml. The two lines that matter for this deployment:

[broker]
external_url = "mqtt://127.0.0.1:1883"
# The [[broker.clients]] entries below are *not* used by the embedded
# broker (because external_url is set) — they're consumed by hc-cli to
# generate the Mosquitto ACL + password files.

[[broker.clients]]
id = "internal.core"
password = "<strong plaintext password for homeCore itself>"
allow_pub = ["homecore/#"]
allow_sub = ["homecore/#"]

[[broker.clients]]
id = "plugin.thermostat"
password = "<strong plaintext password>"
allow_pub = ["homecore/devices/thermostat_+/state",
"homecore/plugins/plugin.thermostat/+",
"homecore/devices/+/cmd"]
allow_sub = ["homecore/devices/thermostat_+/cmd",
"homecore/devices/+/state",
"homecore/devices/+/state/partial"]

# ...one [[broker.clients]] block per plugin.

Lock it down:

sudo chown root:homecore /etc/homecore/homecore.toml
sudo chmod 0640 /etc/homecore/homecore.toml

4. Generate Mosquitto config from homecore.toml

sudo -u homecore hc-cli broker generate-mosquitto-config \
--config /etc/homecore/homecore.toml \
--out-dir /tmp/mosquitto-config

This produces three files under /tmp/mosquitto-config/:

  • mosquitto.conf — listener + references to /mosquitto/config/acl and /mosquitto/config/passwd
  • aclfile — per-user topic rules derived from allow_pub / allow_sub
  • passwd.setup.sh — helper script with mosquitto_passwd commands

Adjust paths for a native (non-Docker) Mosquitto

The generated mosquitto.conf assumes Docker's /mosquitto/config/... layout. For a native systemd install we want /etc/mosquitto/...:

sed -i 's#/mosquitto/config/#/etc/mosquitto/#g' /tmp/mosquitto-config/mosquitto.conf
sed -i 's#/mosquitto/data/#/var/lib/mosquitto/#g' /tmp/mosquitto-config/mosquitto.conf

Populate real passwords

Edit passwd.setup.sh and replace each CHANGE_ME_<ID> with the same plaintext you put in the matching [[broker.clients]].password field. Run the script — Mosquitto's mosquitto_passwd hashes the entries with PBKDF2-SHA512:

cd /tmp/mosquitto-config
bash passwd.setup.sh # produces `passwd` next to the script

Install the Mosquitto files

sudo install -m 0644 /tmp/mosquitto-config/mosquitto.conf /etc/mosquitto/
sudo install -m 0640 -g mosquitto /tmp/mosquitto-config/aclfile /etc/mosquitto/acl
sudo install -m 0600 -g mosquitto /tmp/mosquitto-config/passwd /etc/mosquitto/passwd
rm -rf /tmp/mosquitto-config

acl and passwd must be readable by the mosquitto user; not world-readable (passwd contains hashed creds).

5. Install the HomeCore systemd unit

A template lives at core/scripts/service-templates/homecore.service. Fill in the placeholders for a systemd install:

# /etc/systemd/system/homecore.service
[Unit]
Description=HomeCore Home Automation Server
Documentation=https://github.com/homeCore-io/homeCore
After=network-online.target mosquitto.service
Wants=network-online.target
Requires=mosquitto.service

[Service]
Type=simple
User=homecore
Group=homecore
Environment="HOMECORE_HOME=/var/lib/homecore"
ExecStart=/usr/local/bin/homecore --config /etc/homecore/homecore.toml
ExecReload=/bin/kill -HUP $MAINPID

Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5

KillMode=control-group
KillSignal=SIGINT
TimeoutStopSec=30

StandardOutput=journal
StandardError=journal
SyslogIdentifier=homecore

# Hardening — safe defaults for a system service.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/lib/homecore /var/log/homecore
PrivateTmp=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target

Key bits:

  • Requires=mosquitto.service + After=mosquitto.service — HomeCore will not start until Mosquitto is up, and it restarts when Mosquitto restarts.
  • ReadWritePaths= limits writable directories after ProtectSystem=strict. If you enable log pruning or backups, add those paths here.
  • HOMECORE_HOME tells HomeCore where to resolve relative paths in homecore.toml. Absolute paths in config are safer; the env var is a fallback.

6. Start everything

sudo systemctl daemon-reload
sudo systemctl enable --now mosquitto
sudo systemctl enable --now homecore

7. Verify

Mosquitto is up and authenticating

journalctl -u mosquitto -f --since "2 min ago"

Expect a line like mosquitto version 2.x starting. A New connection from 127.0.0.1 line (with no username) followed by rejected is expected — the homecore client presents creds on its next retry.

Try a denied action to prove the ACL is live:

mosquitto_pub -h 127.0.0.1 -u plugin.thermostat \
-P "<thermostat plaintext password>" \
-t "homecore/devices/zwave_37/cmd" -m '{"on":true}' -d

Since plugin.thermostat does have homecore/devices/+/cmd in its allow_pub, this should succeed. Now try a topic outside its ACL:

mosquitto_pub -h 127.0.0.1 -u plugin.thermostat \
-P "<password>" \
-t "homecore/plugins/plugin.zigbee/register" -m '{}' -d

Expect Mosquitto to log ACL denying access … and the publish to be dropped. That's the difference vs. the embedded broker: enforcement.

HomeCore is up

journalctl -u homecore -f --since "2 min ago"
curl http://127.0.0.1:8080/api/v1/health

Expect {"status":"ok","version":"..."}.

Plugins connect

Each plugin logs its Mosquitto connect at INFO. Tail their logs or, from Mosquitto's side:

journalctl -u mosquitto -g 'New client connected'

Each entry should show the plugin's client_id (e.g. plugin.lutron). If you see Client rejected — not authorised in Mosquitto's log, the plugin is presenting the wrong password — check that its own config file carries the same plaintext you put in passwd.setup.sh.


Operations

Rotating a plugin password

  1. Generate a new plaintext secret.
  2. Update the plugin's own config file with it.
  3. Update the matching [[broker.clients]].password in /etc/homecore/homecore.toml.
  4. Regenerate + re-install the Mosquitto password file:
    sudo -u homecore hc-cli broker generate-mosquitto-config \
    --config /etc/homecore/homecore.toml \
    --out-dir /tmp/mc --force
    # Edit passwd.setup.sh, replace placeholders, run it, copy `passwd`.
    sudo install -m 0600 -g mosquitto /tmp/mc/passwd /etc/mosquitto/passwd
  5. sudo systemctl reload mosquitto — picks up the new passwd without disconnecting already-authenticated clients. The new password takes effect on the next CONNECT. If you want to force an immediate re-auth, sudo systemctl restart mosquitto instead.

Changing a plugin's allowed topics

Edit the allow_pub / allow_sub arrays in [[broker.clients]]. Regenerate only the ACL file:

sudo -u homecore hc-cli broker generate-mosquitto-config \
--config /etc/homecore/homecore.toml \
--out-dir /tmp/mc --force
sudo install -m 0640 -g mosquitto /tmp/mc/aclfile /etc/mosquitto/acl
sudo systemctl reload mosquitto

Mosquitto re-reads the ACL on SIGHUP (what systemctl reload sends).

Adding a new plugin

  1. Add a new [[broker.clients]] block in homecore.toml.
  2. Regenerate everything (generate-mosquitto-config).
  3. Update the passwd (run passwd.setup.sh with the new mosquitto_passwd -b passwd <new-plugin-id> <password> line, or just run the full script with -c removed from the first line so it appends rather than clobbers).
  4. Install the updated acl and passwd files; systemctl reload mosquitto.
  5. Install the plugin binary + its own config (pointing at the same plaintext password).

Troubleshooting

HomeCore refuses to start — broker external_url unreachable. Mosquitto didn't start or is listening on a different port. Check ss -tlnp | grep 1883.

Plugins connect but "Publish denied" fills the Mosquitto log. The plugin is trying a topic outside its ACL. Either the ACL is too narrow (fix [[broker.clients]].allow_pub), or the plugin is misbehaving. Cross-reference against the plugin's documented topic contract.

Plugin can authenticate but homeCore never sees its state. The plugin's allow_pub doesn't cover its state topic. Very common pitfall after renaming a device ID — the ACL patterns stayed narrow.

Mosquitto's log is empty. Default Mosquitto log level is high. Add log_type debug to mosquitto.conf temporarily when debugging.

Next steps