殷忆枫头像

告别依赖地狱!在 Ubuntu 上用 Docker 极速部署 Home Assistant 与 MQTT,并接入 Siri 与嵌入式 C 终端

若该文为原创文章,转载请注明原文出处。

摘要:在旧版 Ubuntu 系统上通过源码或 Conda 部署 Home Assistant,往往会陷入无尽的 Python 依赖冲突(如 josepy、cryptography 版本不兼容)。本文记录了如何放弃源码编译,转投 Docker 阵营,利用 Snap 快速安装 Docker,并成功绕过国内网络限制,优雅部署 Home Assistant 与 Mosquitto (MQTT),最终实现从 Siri 语音控制到 Python 设备端程序自动响应的完整闭环。

一、 为什么选择 Docker 与 Snap?

起初,我们在 Ubuntu 20.04/18.04 上使用 Conda 创建 Python 虚拟环境来部署 HA,但很快就遭遇了严重的依赖冲突:

  1. setuptools 新版本移除了 pkg_resources,导致旧版 HA 启动崩溃。

  2. pip 解析依赖时,自动将 cryptography、pyOpenSSL、josepy 升级到了不兼容的新版本。

  3. ghcr.io 拉取超时,国内网络访问极度不稳定。

破局思路:使用 Docker 进行容器化隔离,彻底摆脱宿主机 Python 环境的污染。由于测试机是 Ubuntu 18.04 (bionic),系统自带的 APT 源和证书相对老旧,直接配置官方 Docker APT 源时常报错。因此,我们选择了 Ubuntu 原生的 Snap 包管理器来安装 Docker,这是最快、最省心的方式。

二、 环境准备

  • 操作系统:Ubuntu 18.04.2 LTS (虚拟机)

  • 核心组件:Docker 29.8.0 (Snap 安装), Docker Compose (内置插件)

  • 目标应用:Home Assistant (稳定版) + Mosquitto (MQTT Broker)

三、 实战部署步骤

1. 使用 Snap 安装 Docker

跳过复杂的 APT 源配置,直接执行:

sudo snap install docker

(首次运行 sudo docker run hello-world 大概率会报网络超时,下一步解决。)

2. 配置 Snap 版 Docker 镜像加速

关键坑点:Snap 版 Docker 的配置路径在 /var/snap/,且必须用 snap restart 重启。

sudo mkdir -p /var/snap/docker/current/config
sudo vim /var/snap/docker/current/config/daemon.json

写入以下内容:

{
  "registry-mirrors": [
    "https://docker.xuanyuan.me",
    "https://docker.1ms.run",
    "https://docker.m.daocloud.io",
    "https://dockerproxy.cn"
  ]
}

重启并验证:

sudo snap restart docker
sudo docker info # 查看 Registry Mirrors 是否包含配置的地址

3. 编写 Docker Compose 编排文件

创建目录和配置,允许 Mosquitto 匿名访问:

mkdir -p ~/ha-docker/mosquitto/config
cd ~/ha-docker
cat > ~/ha-docker/mosquitto/config/mosquitto.conf << 'EOF'
listener 1883 0.0.0.0
allow_anonymous true
EOF

编写 docker-compose.yaml。注意:为了让 iPhone 的 HomeKit Bridge 能够顺利通过 mDNS 发现设备,必须使用 network_mode: host。

services:
  homeassistant:
    container_name: homeassistant
    image: "ghcr.nju.edu.cn/home-assistant/home-assistant:stable"
    volumes:
      - ./config:/config
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped
    privileged: true
    network_mode: host
​
  mosquitto:
    container_name: mosquitto
    image: eclipse-mosquitto:2
    restart: unless-stopped
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
    network_mode: host

4. 解决 GitHub 镜像拉取失败

将 image 字段的 ghcr.io 替换为国内高校的镜像代理(如南京大学镜像站 ghcr.nju.edu.cn),然后启动:

sudo docker compose up -d

四、 配置 Home Assistant 与 MQTT 实体

浏览器访问 http://<虚拟机IP>:8123 初始化 HA。前往 设置 -> 设备与服务 -> 添加集成 -> MQTT,Broker 填 127.0.0.1,端口 1883。

编辑宿主机映射出来的配置文件 ~/ha-docker/config/configuration.yaml,添加虚拟传感器和真实物理设备配置。 关键避坑点:YAML 顶层 mqtt: 只能出现一次,必须合并。

mqtt:
  sensor:
    - name: "MQTT Test Sensor"
      state_topic: "home/test/sensor"
      unit_of_measurement: "°C"
  switch:
    # 虚拟测试开关
    - name: "Siri Test Switch"
      state_topic: "home/siri/switch/state"
      command_topic: "home/siri/switch/set"
      payload_on: "ON"
      payload_off: "OFF"
      retain: true
    # --- 真实物理设备(模拟) ---
    - name: "Physical Relay Switch"
      unique_id: "physical_relay_01"
      state_topic: "my_device/switch/state"
      command_topic: "my_device/switch/set"
      availability_topic: "my_device/switch/status"
      payload_on: "ON"
      payload_off: "OFF"
      payload_available: "online"
      payload_not_available: "offline"
      optimistic: false  # 关键:设备必须真实回报状态,HA才更新界面
      retain: true

保存后,重启 HA 容器:sudo docker restart homeassistant。

五、 解决“设备不可用”问题

重启后,前往 HA 的 MQTT -> 实体,你会发现 Physical Relay Switch 显示为 “不可用”。因为我们配置了 availability_topic,HA 在等设备发“上线”心跳。

在终端手动发一条上线消息:

sudo docker exec -it mosquitto mosquitto_pub -h localhost -t "my_device/switch/status" -m "online" -r

刷新网页,开关变为可用。

六、 设备端 Python 脚本模拟真实硬件(核心)

真实的智能家居硬件(如 ESP8266)内部运行着程序,自动完成“连网 -> 订阅指令 -> 执行 -> 反馈状态”。为了脱离手动敲命令,我们在 Ubuntu 宿主机上写一个 Python 脚本,充当真正的 MQTT 设备。

1. 安装 Python MQTT 库

pip install paho-mqtt

2. 编写模拟设备脚本

新建文件 simulate_device.py 并写入以下代码:

#!/usr/bin/env python3
import paho.mqtt.client as mqtt
​
# --- 配置 ---
MQTT_BROKER = "127.0.0.1"  # 本地运行
MQTT_TOPIC_SET = "my_device/switch/set"
MQTT_TOPIC_STATE = "my_device/switch/state"
MQTT_TOPIC_STATUS = "my_device/switch/status"
​
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("✅ 成功连接到 MQTT Broker")
        # 告诉 HA 设备上线了
        client.publish(MQTT_TOPIC_STATUS, "online", retain=True)
        print("📡 已上报设备状态: online")
        # 订阅控制指令
        client.subscribe(MQTT_TOPIC_SET)
        print(f"👂 正在监听控制指令: {MQTT_TOPIC_SET}")
    else:
        print(f"❌ 连接失败,返回码: {rc}")
​
def on_message(client, userdata, msg):
    payload = msg.payload.decode('utf-8').strip().upper()
    print(f"\n⚡ 收到来自 HA 的指令: {payload}")
    
    # 模拟物理设备动作
    if payload == "ON":
        print("💡 [模拟动作] 继电器吸合,灯亮起")
        # 反馈状态给 HA
        client.publish(MQTT_TOPIC_STATE, "ON", retain=True)
        print("📤 已向 HA 反馈状态: ON")
    elif payload == "OFF":
        print("🌑 [模拟动作] 继电器断开,灯熄灭")
        client.publish(MQTT_TOPIC_STATE, "OFF", retain=True)
        print("📤 已向 HA 反馈状态: OFF")
​
# 初始化 MQTT 客户端
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
​
# 设置遗嘱消息(如果脚本崩溃断开,HA会知道设备离线)
client.will_set(MQTT_TOPIC_STATUS, payload="offline", retain=True)
​
print("🚀 正在启动虚拟设备...")
client.connect(MQTT_BROKER, 1883, 60)
client.loop_forever() # 阻塞程序,持续监听

3. 运行并测试闭环控制

  1. 终端运行脚本:python3 simulate_device.py。此时设备自动上线并进入监听状态。

  2. 前往 HA 网页,点击 Physical Relay Switch。

  3. 终端表现:脚本瞬间打印出 收到指令: ON 以及 反馈状态: ON。

  4. 网页表现:开关稳稳停留在开启状态,完美实现自动化闭环。

(注:如果你将 MQTT_BROKER 换成局域网 IP,并将此脚本放到树莓派上,配合 RPi.GPIO 库,就是一个真正的智能家居控制中枢了。)

七、 进阶:嵌入式 Linux 纯 C 语言设备端实现(完整版)

在实际的嵌入式开发中(如 SSD202D 芯片),直接引入 Paho 库并交叉编译过于笨重。这里提供一份仅依赖原生 Socket、多线程、且包含 LWT(遗嘱消息) 的完整版 Ha_mqtt.c。

1. Ha_mqtt.h 头文件

c

#ifndef __Ha_mqtt_H__
#define __Ha_mqtt_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>

#ifdef __cplusplus
extern "C" {
#endif

#define cMqttTxBufferSize  0x300
#define cMqttRxBufferSize  0x400
#define cMqttDataNum       16

#define SET 1
#define CLEAR 0
#define DEBUG_PRINTF printf

typedef struct 
{
    unsigned char InitOk : 1;   // 1:断开/失败   0:连接成功
    unsigned char ConnOk : 1;   // 1:已连接     0:未连接
} MqttBits;

typedef struct  
{  
    unsigned short Len;                         // 数据长度
    unsigned char  Data[cMqttTxBufferSize];     // 数据报文内容
} MqttTxDataStru; 

typedef struct 
{
    int   SockFd;                               // Socket 文件描述符
    unsigned char TxDataInPtr;                  // 发送环形队列写入指针
    unsigned char TxDataOutPtr;                 // 发送环形队列读取指针
    MqttTxDataStru TxDataBuf[cMqttDataNum];     // 发送环形队列
    unsigned short LastPingTime;                // 上次心跳时间
    unsigned short ReconnectTime;               // 上次重连时间
} MqttStru;

extern MqttBits  gbitMqtt;
extern MqttStru  gstvMqtt;

void Start_MqttDeal_Monitor_Thread(void);
int MqttDealStatus(void);
void MqttDeal_InputData(unsigned char *Data, unsigned short len);

#ifdef __cplusplus
}
#endif

#endif

2. Ha_mqtt.c 完整源码

(注意:代码中的 TimerGet1msInc 和 TimerGet1msSubb 是你系统内自带的毫秒级定时器函数,请确保头文件中已包含)

c

#include "Ha_mqtt.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/prctl.h>

#ifdef __cplusplus
extern "C" {
#endif

#define MQTT_BROKER_IP      "192.168.52.136"    // 修改为你的 HA IP
#define MQTT_BROKER_PORT    1883
#define MQTT_CLIENT_ID      "SSD202D_Panel"
#define MQTT_TOPIC_SET      "my_device/switch/set"
#define MQTT_TOPIC_STATE    "my_device/switch/state"
#define MQTT_TOPIC_STATUS   "my_device/switch/status"

#define MQTT_PAYLOAD_ONLINE  "online"
#define MQTT_PAYLOAD_OFFLINE "offline"

MqttBits  gbitMqtt;
MqttStru  gstvMqtt;
static pthread_mutex_t sg_mqtt_mutex = PTHREAD_MUTEX_INITIALIZER;

static void *MqttDeal_Main_Thread(void *arg);
static void MqttDeal_Connect(void);
static void MqttDeal_Process_Packet(unsigned char *buf, int len);
static int build_connect_packet(unsigned char *buf);
static int build_subscribe_packet(unsigned char *buf, const char *topic);
static int build_publish_packet(unsigned char *buf, const char *topic, const unsigned char *payload, int payload_len, int retain);

void Start_MqttDeal_Monitor_Thread(void)
{
    pthread_t mqtt_tid;
    DEBUG_PRINTF("==================================================\n");
    DEBUG_PRINTF("[MQTT Thread] Start MQTT Monitor Thread...\n");
    DEBUG_PRINTF("==================================================\n");

    memset(&gbitMqtt, 0, sizeof(MqttBits));
    memset(&gstvMqtt, 0, sizeof(MqttStru));
    
    gbitMqtt.InitOk = SET;
    gstvMqtt.SockFd = -1;

    if(pthread_create(&mqtt_tid, NULL, MqttDeal_Main_Thread, NULL) < 0) {
        perror("[MQTT Error] pthread_create failed");
    }
}

static void *MqttDeal_Main_Thread(void *arg)
{
    unsigned char RxBuf[cMqttRxBufferSize] = {0};
    fd_set r_fdset;
    struct timeval timeout;

    pthread_detach(pthread_self());
    prctl(PR_SET_NAME, "MqttDealMainThread");

    while(1) {
        if (gstvMqtt.SockFd < 0) {
            if (TimerGet1msSubb(gstvMqtt.ReconnectTime, 5000)) {
                MqttDeal_Connect();
                gstvMqtt.ReconnectTime = TimerGet1msInc();
            }
            sleep(1);
            continue;
        }

        pthread_mutex_lock(&sg_mqtt_mutex);
        int out = gstvMqtt.TxDataOutPtr;
        int len = gstvMqtt.TxDataBuf[out].Len;
        unsigned char *data = gstvMqtt.TxDataBuf[out].Data;
        pthread_mutex_unlock(&sg_mqtt_mutex);

        if (len > 0) {
            unsigned char pub_buf[cMqttTxBufferSize];
            int pub_len = build_publish_packet(pub_buf, MQTT_TOPIC_STATE, data, len, 0);
            DEBUG_PRINTF("[MQTT TX] -> Reporting state to HA: %.*s\n", len, data);
            send(gstvMqtt.SockFd, pub_buf, pub_len, MSG_NOSIGNAL);

            pthread_mutex_lock(&sg_mqtt_mutex);
            gstvMqtt.TxDataBuf[out].Len = 0;
            gstvMqtt.TxDataOutPtr = (out + 1) % cMqttDataNum;
            pthread_mutex_unlock(&sg_mqtt_mutex);
        }

        if (TimerGet1msSubb(gstvMqtt.LastPingTime, 30000)) {
            DEBUG_PRINTF("[MQTT Ping] Sending PINGREQ\n");
            unsigned char ping_pkt[2] = {0xC0, 0x00};
            send(gstvMqtt.SockFd, ping_pkt, 2, MSG_NOSIGNAL);
            gstvMqtt.LastPingTime = TimerGet1msInc();
        }

        FD_ZERO(&r_fdset);
        FD_SET(gstvMqtt.SockFd, &r_fdset);
        timeout.tv_sec = 0;
        timeout.tv_usec = 100 * 1000;

        int ret = select(gstvMqtt.SockFd + 1, &r_fdset, NULL, NULL, &timeout);
        if (ret > 0) {
            int recv_len = recv(gstvMqtt.SockFd, RxBuf, sizeof(RxBuf), 0);
            if (recv_len > 0) {
                MqttDeal_Process_Packet(RxBuf, recv_len);
            } else if (recv_len == 0 || (recv_len < 0 && errno != EAGAIN)) {
                DEBUG_PRINTF("[MQTT Warn] Disconnected! Preparing to reconnect...\n");
                close(gstvMqtt.SockFd);
                gstvMqtt.SockFd = -1;
                gbitMqtt.InitOk = SET;
            }
        }
    }
    pthread_exit(0);
}

static void MqttDeal_Connect(void)
{
    struct sockaddr_in broker_addr;
    unsigned char buf[512] = {0};
    int len;

    DEBUG_PRINTF("[MQTT Connect] Trying to connect to Broker %s:%d ...\n", MQTT_BROKER_IP, MQTT_BROKER_PORT);

    gstvMqtt.SockFd = socket(AF_INET, SOCK_STREAM, 0);
    if (gstvMqtt.SockFd < 0) return;

    broker_addr.sin_family = AF_INET;
    broker_addr.sin_port = htons(MQTT_BROKER_PORT);
    inet_pton(AF_INET, MQTT_BROKER_IP, &broker_addr.sin_addr);

    if (connect(gstvMqtt.SockFd, (struct sockaddr *)&broker_addr, sizeof(broker_addr)) < 0) {
        DEBUG_PRINTF("[MQTT Error] Connect failed (Broker down or network issue)\n");
        close(gstvMqtt.SockFd);
        gstvMqtt.SockFd = -1;
        return;
    }

    len = build_connect_packet(buf);
    send(gstvMqtt.SockFd, buf, len, MSG_NOSIGNAL);
    recv(gstvMqtt.SockFd, buf, sizeof(buf), 0);

    len = build_subscribe_packet(buf, MQTT_TOPIC_SET);
    send(gstvMqtt.SockFd, buf, len, MSG_NOSIGNAL);
    recv(gstvMqtt.SockFd, buf, sizeof(buf), 0);

    unsigned char online_buf[256];
    int online_len = build_publish_packet(online_buf, MQTT_TOPIC_STATUS, (const unsigned char *)MQTT_PAYLOAD_ONLINE, strlen(MQTT_PAYLOAD_ONLINE), 1);
    send(gstvMqtt.SockFd, online_buf, online_len, MSG_NOSIGNAL);
    DEBUG_PRINTF("[MQTT Online] Sent online notification to HA: online (Retained)\n");

    gbitMqtt.InitOk = CLEAR;
    gbitMqtt.ConnOk = SET;
    gstvMqtt.LastPingTime = TimerGet1msInc();

    DEBUG_PRINTF("[MQTT Success] Connected and Subscribed! Waiting for HA commands...\n");
}

static void MqttDeal_Process_Packet(unsigned char *buf, int len)
{
    if (len < 5) return;

    if ((buf[0] & 0xF0) == 0x30) {
        int pos = 1;
        while (pos < len && (buf[pos] & 0x80)) pos++;
        pos++;
        
        int topic_len = (buf[pos] << 8) | buf[pos+1];
        pos += 2 + topic_len;
        
        int payload_len = len - pos;
        if (payload_len > 0) {
            char payload[128] = {0};
            if (payload_len > 127) payload_len = 127;
            memcpy(payload, &buf[pos], payload_len);
            
            DEBUG_PRINTF("[MQTT RX] <- Received HA command: %s\n", payload);
            
            if (strcmp(payload, "ON") == 0 || strcmp(payload, "OFF") == 0) {
                DEBUG_PRINTF("[Hardware Action] Simulating relay execution: %s\n", payload);
                MqttDeal_InputData((unsigned char *)payload, strlen(payload));
            }
        }
    }
}

int MqttDealStatus(void)
{
    pthread_mutex_lock(&sg_mqtt_mutex);
    int ret = gbitMqtt.InitOk;
    pthread_mutex_unlock(&sg_mqtt_mutex);
    return ret;
}

void MqttDeal_InputData(unsigned char *Data, unsigned short len)
{
    if (len == 0 || len > cMqttTxBufferSize) return;

    pthread_mutex_lock(&sg_mqtt_mutex);
    unsigned char next_in = (gstvMqtt.TxDataInPtr + 1) % cMqttDataNum;
    if (next_in == gstvMqtt.TxDataOutPtr) {
        DEBUG_PRINTF("[MQTT Warn] TX queue full, dropping data\n");
        pthread_mutex_unlock(&sg_mqtt_mutex);
        return;
    }

    int idx = gstvMqtt.TxDataInPtr;
    gstvMqtt.TxDataBuf[idx].Len = len;
    memcpy(gstvMqtt.TxDataBuf[idx].Data, Data, len);
    gstvMqtt.TxDataInPtr = next_in;
    pthread_mutex_unlock(&sg_mqtt_mutex);
}

static int encode_remaining_length(unsigned char *buf, int length) {
    int i = 0;
    do {
        unsigned char byte = length % 128;
        length = length / 128;
        if (length > 0) byte = byte | 0x80;
        buf[i++] = byte;
    } while (length > 0);
    return i;
}

static int build_connect_packet(unsigned char *buf) {
    int pos = 0;
    buf[pos++] = 0x10;
    
    int client_id_len = strlen(MQTT_CLIENT_ID);
    int will_topic_len = strlen(MQTT_TOPIC_STATUS);
    int will_payload_len = strlen(MQTT_PAYLOAD_OFFLINE);
    
    int len = 10 + (2 + client_id_len) + (2 + will_topic_len) + (2 + will_payload_len);
    
    unsigned char len_bytes[4];
    int len_size = encode_remaining_length(len_bytes, len);
    memcpy(&buf[pos], len_bytes, len_size); pos += len_size;
    
    buf[pos++] = 0x00; buf[pos++] = 0x04;
    memcpy(&buf[pos], "MQTT", 4); pos += 4;
    buf[pos++] = 0x04;
    buf[pos++] = 0x26; // CleanSession(0x02) + WillFlag(0x04) + WillRetain(0x20)
    buf[pos++] = 0x00; buf[pos++] = 0x3C; 
    
    buf[pos++] = (client_id_len >> 8) & 0xFF; buf[pos++] = client_id_len & 0xFF;
    memcpy(&buf[pos], MQTT_CLIENT_ID, client_id_len); pos += client_id_len;
    
    buf[pos++] = (will_topic_len >> 8) & 0xFF; buf[pos++] = will_topic_len & 0xFF;
    memcpy(&buf[pos], MQTT_TOPIC_STATUS, will_topic_len); pos += will_topic_len;
    
    buf[pos++] = (will_payload_len >> 8) & 0xFF; buf[pos++] = will_payload_len & 0xFF;
    memcpy(&buf[pos], MQTT_PAYLOAD_OFFLINE, will_payload_len); pos += will_payload_len;
    
    return pos;
}

static int build_subscribe_packet(unsigned char *buf, const char *topic) {
    int pos = 0;
    buf[pos++] = 0x82;
    int topic_len = strlen(topic);
    int len = 2 + 2 + topic_len + 1;
    unsigned char len_bytes[4];
    int len_size = encode_remaining_length(len_bytes, len);
    memcpy(&buf[pos], len_bytes, len_size); pos += len_size;
    
    buf[pos++] = 0x00; buf[pos++] = 0x01;
    buf[pos++] = 0x00; buf[pos++] = topic_len;
    memcpy(&buf[pos], topic, topic_len); pos += topic_len;
    buf[pos++] = 0x00;
    return pos;
}

static int build_publish_packet(unsigned char *buf, const char *topic, const unsigned char *payload, int payload_len, int retain) {
    int pos = 0;
    buf[pos++] = 0x30 | (retain ? 0x01 : 0x00); 
    
    int topic_len = strlen(topic);
    int len = 2 + topic_len + payload_len;
    unsigned char len_bytes[4];
    int len_size = encode_remaining_length(len_bytes, len);
    memcpy(&buf[pos], len_bytes, len_size); pos += len_size;
    
    buf[pos++] = 0x00; buf[pos++] = topic_len;
    memcpy(&buf[pos], topic, topic_len); pos += topic_len;
    memcpy(&buf[pos], payload, payload_len); pos += payload_len;
    return pos;
}

#ifdef __cplusplus
}
#endif

3. 串口实测运行结果

将上述代码交叉编译并烧录至 SSD202D 屏终端,操作 HA 或 Siri 控制时,终端输出完美闭环日志:

text

[14:48:09.913] [MQTT RX] <- Received HA command: ON
[14:48:09.914] [Hardware Action] Simulating relay execution: ON
[14:48:09.915] [MQTT TX] -> Reporting state to HA: ON

[14:48:11.736] [MQTT RX] <- Received HA command: OFF
[14:48:11.737] [Hardware Action] Simulating relay execution: OFF
[14:48:11.738] [MQTT TX] -> Reporting state to HA: OFF

八、 接入 HomeKit 与 Siri 语音控制

  1. 在 HA 网页点击 设置 -> 设备与服务,点击右下角 添加集成,搜索 HomeKit Bridge。

  2. 提交后,找到屏幕弹出的 二维码(如果没有,去左侧菜单 通知 里找)。

  3. iPhone 打开 “家庭” App,点击 “+” -> 添加配件 扫码绑定。

  4. 测试 Siri: 确保刚才的 simulate_device.py 脚本还在运行,对着 iPhone 说:

    “嘿,Siri,打开 Siri Test Switch”

    此时,虽然 Siri 控制的是虚拟开关,但你的 HA 会迅速将指令下达给 MQTT,证明全链路(Siri -> HA -> MQTT -> 设备)已完全打通!

九、 踩坑总结与避坑指南

  1. 摒弃 APT 安装 Docker:在老旧 Ubuntu 系统上,直接 apt install docker-ce 常因 SSL 证书过期失败。Snap 是绝对的救星。

  2. Snap 配置文件路径:牢记 /var/snap/docker/current/config/daemon.json。

  3. GitHub 镜像单独处理:拉取 ghcr.io 时,直接在 compose 文件里换成 ghcr.nju.edu.cn。

  4. 网络模式选择:接 Apple HomeKit,必须用 network_mode: host,否则 mDNS 无法穿透。

  5. HomeKit 扫不到码:确保虚拟机使用桥接网卡(Bridged),保证手机和虚拟机在同一 IP 网段。

  6. YAML 语法极度严格:顶层 mqtt: 全局只能有一个,多个设备必须用列表(-)并列合并。

  7. optimistic: false 的作用:强制 HA 等待设备真实的 state_topic 反馈,避免假开启。

  8. availability_topic 的坑:配置了它,设备必须发布 online 才能激活。嫌麻烦可以直接删掉这行。

测试结果,使用iphone测试,在家庭里控制正常。

十、 踩坑实录:重启 Ubuntu 后 Docker 崩溃无法启动的终极排查

在完成所有配置并成功测试后,我重启了 Ubuntu 虚拟机。结果再次使用时,发现 Home Assistant 无法访问,敲击任何 docker 命令都提示连接不上 Socket:

🚨 1. 问题现象:Docker 服务彻底罢工

执行 sudo docker ps 提示:

text

failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory

执行 sudo systemctl status snap.docker.dockerd 发现服务状态为 failed (Result: exit-code)。
尝试了删除残留 PID 文件、重启 AppArmor 等操作,均无效。服务启动后没几秒就会自动退出。

🔍 2. 问题排查:查看 Snap 底层日志

既然表面上的 systemd 日志看不出核心问题,我们需要查看 Snap 管理的 Docker 守护进程的专属日志:

bash

sudo snap logs docker.dockerd -n 50

在满屏的报错中,终于找到了真正的“元凶”:

text

docker.dockerd[6006]: unable to configure the Docker daemon with file /var/snap/docker/3618/config/daemon.json: the following directives don't match any configuration option: registry-mirrors-ghcr

💡 3. 问题原因:非标准配置项导致解析失败

原因水落石出:在之前的配置中,为了让 ghcr.io 镜像加速,我往 Docker 的全局配置文件 daemon.json 中加入了非标准字段 "registry-mirrors-ghcr"。

Docker 的 daemon.json 对配置项有极其严格的校验机制。官方只认 "registry-mirrors" 用于加速 Docker Hub (docker.io),遇到不认识的 registry-mirrors-ghcr 字段,守护进程直接判定配置损坏,拒绝启动并反复尝试重启,最终被 systemd 强制放弃。

🛠️ 4. 怎么处理:修正 daemon.json

找到日志里提示的具体文件路径(注意,Snap 版本路径里有版本号 3618,请根据你的日志提示替换):

  1. 编辑配置文件:

    bash

    sudo nano /var/snap/docker/3618/config/daemon.json
  2. 将里面多余的非标准字段删掉,只保留官方支持的 "registry-mirrors"。修改后的标准内容如下:

    json

    {
      "registry-mirrors": [
        "https://docker.xuanyuan.me",
        "https://docker.1ms.run",
        "https://docker.m.daocloud.io",
        "https://dockerproxy.cn"
      ]
    }

    (注意:JSON 格式严格,最后一个大括号前不要有逗号)

🚀 5. 怎么启动:恢复所有服务

修改保存后,按以下步骤重新拉起环境:

第一步:重启 Docker 引擎

bash

sudo systemctl restart snap.docker.dockerd

再次查看状态,确保变回 Active: active (running)。

第二步:启动容器
因为你的项目目录 ~/ha-docker 还在,直接进入目录一键拉起:

bash

cd ~/ha-docker
sudo docker compose up -d

第三步:验证

bash

sudo docker ps

看到 homeassistant 和 mosquitto 状态为 Up,就说明完全恢复了!此时去浏览器访问 http://192.168.52.136:8123,一切都回来了。

📌 避坑总结

  1. Docker 镜像加速配置有边界:daemon.json 只能写官方支持的字段。对于 ghcr.io 或 gcr.io 等特殊仓库,千万不要去改 daemon.json。

  2. 特殊镜像的正确处理姿势:就像本文第四部分提到的,拉取 GitHub 镜像时,直接在 docker-compose.yaml 的 image 字段里换成国内代理(如 ghcr.nju.edu.cn)即可,这是最安全、最有效的做法,不会影响 Docker 本身的运行。

如有侵权,或需要完整代码,请及时联系博主。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_38807927/article/details/168442124

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--