若该文为原创文章,转载请注明原文出处。
摘要:在旧版 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,但很快就遭遇了严重的依赖冲突:
-
setuptools新版本移除了pkg_resources,导致旧版 HA 启动崩溃。 -
pip解析依赖时,自动将cryptography、pyOpenSSL、josepy升级到了不兼容的新版本。 -
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. 运行并测试闭环控制
-
终端运行脚本:
python3 simulate_device.py。此时设备自动上线并进入监听状态。 -
前往 HA 网页,点击
Physical Relay Switch。 -
终端表现:脚本瞬间打印出
收到指令: ON以及反馈状态: ON。 -
网页表现:开关稳稳停留在开启状态,完美实现自动化闭环。
(注:如果你将 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 语音控制
-
在 HA 网页点击 设置 -> 设备与服务,点击右下角 添加集成,搜索 HomeKit Bridge。
-
提交后,找到屏幕弹出的 二维码(如果没有,去左侧菜单 通知 里找)。
-
iPhone 打开 “家庭” App,点击 “+” -> 添加配件 扫码绑定。
-
测试 Siri: 确保刚才的
simulate_device.py脚本还在运行,对着 iPhone 说:“嘿,Siri,打开 Siri Test Switch”
此时,虽然 Siri 控制的是虚拟开关,但你的 HA 会迅速将指令下达给 MQTT,证明全链路(Siri -> HA -> MQTT -> 设备)已完全打通!
九、 踩坑总结与避坑指南
-
摒弃 APT 安装 Docker:在老旧 Ubuntu 系统上,直接
apt install docker-ce常因 SSL 证书过期失败。Snap 是绝对的救星。 -
Snap 配置文件路径:牢记
/var/snap/docker/current/config/daemon.json。 -
GitHub 镜像单独处理:拉取
ghcr.io时,直接在compose文件里换成ghcr.nju.edu.cn。 -
网络模式选择:接 Apple HomeKit,必须用
network_mode: host,否则 mDNS 无法穿透。 -
HomeKit 扫不到码:确保虚拟机使用桥接网卡(Bridged),保证手机和虚拟机在同一 IP 网段。
-
YAML 语法极度严格:顶层
mqtt:全局只能有一个,多个设备必须用列表(-)并列合并。 -
optimistic: false的作用:强制 HA 等待设备真实的state_topic反馈,避免假开启。 -
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,请根据你的日志提示替换):
-
编辑配置文件:
bash
sudo nano /var/snap/docker/3618/config/daemon.json
-
将里面多余的非标准字段删掉,只保留官方支持的
"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,一切都回来了。
📌 避坑总结
-
Docker 镜像加速配置有边界:
daemon.json只能写官方支持的字段。对于ghcr.io或gcr.io等特殊仓库,千万不要去改daemon.json。 -
特殊镜像的正确处理姿势:就像本文第四部分提到的,拉取 GitHub 镜像时,直接在
docker-compose.yaml的image字段里换成国内代理(如ghcr.nju.edu.cn)即可,这是最安全、最有效的做法,不会影响 Docker 本身的运行。
如有侵权,或需要完整代码,请及时联系博主。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_38807927/article/details/168442124



