La forma fiable de ejecutar un script Python MQTT al arrancar es un servicio systemd: inicia tu cliente cuando la red ya está activa, lo reinicia si se cae, captura sus logs y lo detiene limpiamente al apagar. En este tutorial convertirás un script de paho-mqtt en un servicio en una Raspberry Pi, guardarás las credenciales fuera del código, resolverás el problema del reloj en el arranque que rompe TLS y aprenderás los comandos para la operación diaria. El mismo archivo de unidad funciona en cualquier distribución Linux moderna.
Por qué systemd y no cron o rc.local
Hay varias formas de iniciar un script al arrancar y no son equivalentes:
| Método | Reinicia si falla | Espera a la red | Logs | Apagado limpio |
|---|---|---|---|---|
@reboot en crontab |
No | No | Solo si rediriges la salida | No |
/etc/rc.local |
No | Poco fiable | No | No |
| Autoarranque del escritorio | No | No | No | Solo con sesión de escritorio |
| Servicio systemd | Sí, configurable | Sí, con ordenación | journald integrado | Sí, SIGTERM y luego SIGKILL |
Una entrada de crontab parece más sencilla, pero el resultado es un gateway MQTT que muere en silencio a las 3 de la madrugada y nunca vuelve. systemd ya está funcionando en Raspberry Pi OS, así que no hay nada que instalar.
Paso 1: preparar el script
Un script que se ejecuta como servicio debe leer la configuración de variables de entorno, reconectarse por sí mismo y salir limpiamente cuando systemd envíe SIGTERM. Este ejemplo publica un heartbeat con el tiempo de actividad cada 30 segundos y usa un Last Will para que los suscriptores vean cuándo el dispositivo se desconecta.
#!/usr/bin/env python3
import json
import os
import signal
import threading
import time
import paho.mqtt.client as mqtt
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.environ["MQTT_CLIENT_ID"] # obligatorio y único
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/s8t4n6/pi1")
INTERVAL = int(os.getenv("PUBLISH_INTERVAL", "30"))
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())
signal.signal(signal.SIGINT, lambda *_: stop.set())
def on_connect(client, userdata, flags, reason_code, properties):
print(f"connected to {HOST}:{PORT} ({reason_code})", flush=True)
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
def on_disconnect(client, userdata, flags, reason_code, properties):
print(f"disconnected ({reason_code}), will retry", flush=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.connect_async(HOST, PORT, keepalive=30)
client.loop_start()
start = time.monotonic()
while not stop.wait(INTERVAL):
payload = {"uptime_s": int(time.monotonic() - start), "ts": int(time.time())}
client.publish(f"{PREFIX}/heartbeat", json.dumps(payload), qos=1)
print("stopping", flush=True)
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(3)
client.disconnect()
client.loop_stop()
Aquí connect_async() es importante. Un connect() normal lanza una excepción si el broker no es accesible en ese instante, algo habitual en los primeros segundos tras el arranque. connect_async() delega la conexión en el bucle de red, que reintenta con el backoff definido por reconnect_delay_set().
Paso 2: instalarlo en una ubicación estable
No ejecutes servicios desde la carpeta de Descargas de tu directorio personal. Dale al gateway su propio directorio, entorno virtual y usuario de sistema:
sudo useradd --system --home /opt/mqtt-client --shell /usr/sbin/nologin mqttclient
sudo mkdir -p /opt/mqtt-client
sudo cp heartbeat.py /opt/mqtt-client/
sudo python3 -m venv /opt/mqtt-client/.venv
sudo /opt/mqtt-client/.venv/bin/pip install "paho-mqtt>=2.0"
sudo chown -R mqttclient:mqttclient /opt/mqtt-client
Si el script usa GPIO, I2C o puertos serie, añade el usuario al grupo correspondiente, por ejemplo sudo usermod -aG gpio,i2c,dialout mqttclient.
Paso 3: guardar las credenciales en un archivo de entorno
sudo mkdir -p /etc/mqtt-client
sudo tee /etc/mqtt-client/env > /dev/null <<'EOF'
MQTT_HOST=broker.connectmq.com
MQTT_PORT=1883
MQTT_CLIENT_ID=pi1-heartbeat-s8t4n6
TOPIC_PREFIX=connectmq/demo/s8t4n6/pi1
PUBLISH_INTERVAL=30
EOF
sudo chmod 600 /etc/mqtt-client/env
Para la primera prueba, esto apunta al servidor MQTT público gratuito, que no necesita credenciales y está pensado para pruebas, no para datos de producción. systemd lee el archivo como root antes de iniciar el proceso, así que puede quedar legible solo para root.
Paso 4: escribir el archivo de unidad para ejecutar el script Python MQTT al arrancar
Crea /etc/systemd/system/mqtt-client.service:
[Unit]
Description=Python MQTT client
Wants=network-online.target time-sync.target
After=network-online.target time-sync.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=mqttclient
Group=mqttclient
WorkingDirectory=/opt/mqtt-client
EnvironmentFile=/etc/mqtt-client/env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mqtt-client/.venv/bin/python /opt/mqtt-client/heartbeat.py
Restart=always
RestartSec=5
TimeoutStopSec=15
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
Qué hace cada línea importante:
Wants/After=network-online.targetretrasa el inicio hasta que la red está configurada, y no solo hasta que el servicio de red ha arrancado.time-sync.targetespera al reloj (consulta la nota sobre TLS más abajo).StartLimitIntervalSec=0desactiva el límite de frecuencia de arranque, para que systemd siga reintentando indefinidamente en lugar de rendirse tras unos cuantos fallos rápidos.Restart=alwaysyRestartSec=5reinician el script cinco segundos después de cualquier salida, incluidos los fallos y las salidas limpias.PYTHONUNBUFFERED=1hace que la salida deprint()aparezca de inmediato en el journal.- Las opciones
Protect*aíslan el proceso en un sandbox; la checklist para asegurar una Raspberry Pi IoT las explica con más detalle.
Habilítalo e inícialo:
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-client.service
systemctl status mqtt-client.service
Paso 5: verificar y operar
Observa los logs en vivo y después suscríbete desde otra máquina para confirmar que llegan los mensajes:
journalctl -u mqtt-client.service -f
mosquitto_sub -h broker.connectmq.com -p 1883 -i boot-viewer-s8t4n6 -t "connectmq/demo/s8t4n6/pi1/#" -v
Ahora reinicia la Pi con sudo reboot. El suscriptor ve offline (por el apagado limpio) y después online y los heartbeats cuando la Pi vuelve. Para probar la recuperación ante fallos, mata el proceso con sudo systemctl kill -s SIGKILL mqtt-client.service y observa cómo systemd lo reinicia a los cinco segundos.
Comandos que usarás a diario:
| Tarea | Comando |
|---|---|
| Reiniciar tras cambiar el código o el entorno | sudo systemctl restart mqtt-client |
| Detener temporalmente | sudo systemctl stop mqtt-client |
| Desactivar en el arranque | sudo systemctl disable mqtt-client |
| Logs desde el último arranque | journalctl -u mqtt-client -b |
| Mostrar la unidad efectiva | systemctl cat mqtt-client |
La trampa de TLS y el reloj
La mayoría de los modelos de Raspberry Pi no traen un reloj de tiempo real con batería (la Pi 5 tiene RTC, pero necesita una batería para mantener la hora sin alimentación). Al arrancar, el reloj parte de la última hora guardada hasta que NTP sincroniza. Si está muy desfasado, la validación del certificado TLS falla porque el certificado parece todavía no válido o caducado, y tu script registra errores de conexión repetidos en el puerto 8883.
La ordenación con time-sync.target solo ayuda si un servicio realmente espera a la sincronización. Habilita el que lo hace:
sudo systemctl enable systemd-time-wait-sync.service
timedatectl status
Con eso habilitado, el cliente MQTT solo arranca cuando el reloj está sincronizado. Como el script también se reconecta por sí mismo, se recupera incluso sin ello, solo que más despacio.
Solución de problemas de un servicio que no se mantiene activo
Cuando un servicio se comporta mal, systemctl status y el journal casi siempre contienen la respuesta. Estos son los fallos más frecuentes con clientes MQTT en Python:
status=203/EXEC: systemd no puede ejecutarExecStart. La ruta al Python del venv es incorrecta, o el venv lo creó otro usuario y no es legible. Usa rutas absolutas y compruébalas conls -l.ModuleNotFoundError: paho: la unidad ejecuta el Python del sistema en lugar del venv. ApuntaExecStarta/opt/mqtt-client/.venv/bin/python, no apython3.KeyError: 'MQTT_CLIENT_ID': al archivo de entorno le falta una variable o tiene una errata. Ten en cuenta que las líneas deEnvironmentFileson simplesKEY=valuesinexport.PermissionErroren un archivo o dispositivo:ProtectSystem=strictha dejado la ruta en solo lectura, o al usuario del servicio le falta un grupo. Añade el directorio aReadWritePaths=o el usuario al grupo adecuado.- "connection refused" o "not authorised" repetidos: puerto o credenciales incorrectos, o un client ID duplicado. Dos procesos con el mismo client ID se desconectan mutuamente sin parar, así que asegúrate de que no sigue activa una prueba manual.
- Reinicios cada pocos segundos sin fin: el script sale inmediatamente. Ejecuta a mano el comando exacto de
ExecStartcomo usuario del servicio consudo -u mqttclientpara ver el error en tu terminal.
Pasar a un broker privado
Cuando el servicio funcione, cambia a credenciales de producción. Crea un broker en ConnectMQ y edita /etc/mqtt-client/env:
MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
TOPIC_PREFIX=site1/gateways/pi1
Reinicia el servicio y el script se conectará por TLS. Un broker MQTT gestionado mantiene funcionando el lado del servidor mientras systemd mantiene funcionando el lado del cliente, y juntos cubren la mayoría de las caídas. Para fallos que parecen bloqueos en lugar de salidas, añade un watchdog de aplicación como se describe en los consejos de fiabilidad para Raspberry Pi, y guarda datos en buffer durante los cortes de red con el buffer de mensajes MQTT sin conexión en Python. El manual de systemd.service enumera todas las opciones.
¿Eres nuevo en MQTT con la Pi? Empieza por el tutorial de Raspberry Pi con MQTT y vuelve aquí para hacer permanente tu cliente.
Mantén tu Pi conectada con ConnectMQ
Regístrate en app.connectmq.com en el plan gratuito Serverless, sin tarjeta de crédito, y pega las credenciales generadas en tu archivo de entorno. El portal de ConnectMQ muestra tus dispositivos, así que tras el siguiente reinicio puedes confirmar que tu servicio volvió a estar en línea sin entrar en la Pi.
Preguntas frecuentes
¿Por qué mi servicio arranca antes de que la red esté lista?
After=network.target solo espera al servicio de red, no a una conexión operativa. Usa Wants= y After=network-online.target, y deja que el script se reconecte por sí mismo con connect_async().
¿Por qué no veo ninguna salida en journalctl?
Python almacena stdout en buffer cuando no es una terminal. Define PYTHONUNBUFFERED=1, ejecuta Python con -u o pasa flush=True a print().
¿Debería usar un servicio de usuario en lugar de un servicio de sistema?
Los servicios de usuario solo se ejecutan mientras ese usuario tiene una sesión abierta, salvo que se active lingering. Un servicio de sistema con un User= dedicado es más sencillo y arranca con el sistema sin necesidad de iniciar sesión.
¿Cómo ejecuto varios scripts MQTT?
Crea un archivo de unidad por script o usa una unidad plantilla como mqtt-client@.service con un archivo de entorno distinto por instancia. Cada instancia necesita su propio client ID único.



