Stage 5 of 6 · Theory into practice · programs that stay up
From script to service
Right now your bridge runs because a terminal holds it, and dies when the terminal does: SSH drops, laptop sleeps, Pi reboots, demo over. Mosquitto has none of these problems, and since Week 9 you have known its secret's name without using it: systemd runs it as a service. Today your own program gets the same citizenship, about ten lines of configuration, and your project stops needing a babysitter.
Why your demo dies at night
A program you start from a shell is that shell's child. Close the SSH session and the system hangs up on the whole family (literally: the signal is called SIGHUP, hang up, inherited from the telephone era), and your bridge goes with it. Even surviving that, a crash at 2 am stays crashed, and a power cut on demo morning boots a Pi that runs everything except your project. A service inverts all three defaults: the init system, systemd on Raspberry Pi OS, owns the process instead of a terminal, restarts it if it dies, and starts it at boot before anyone logs in.
systemctl enable; today your bridge moves down to join it.The unit file, line by line
A service is declared by a unit file, a dozen lines of INI-style text in /etc/systemd/system/. Here is a complete one for the Week 11 bridge; swap the paths and the name for your project's program:
bench-bridge.service, annotated. Three sections: what I am and what I come after, how to run me, when to start me. The venv path in ExecStart matters: a service has no shell, no source activate, so the interpreter is named directly, Week 4's venv paying one more dividend.The two walls, pre-announced
Both classic failures are path failures. status=203/EXEC means ExecStart does not point at a real executable, usually a ~ that systemd refuses to expand or a venv path typed from memory; ModuleNotFoundError in the log means the system python ran instead of the venv's. Both are diagnosed the same way: journalctl -u bench-bridge -n 30 and read, the Week 9 reflex.
Install, enable, watch
- Place the unit.
sudo nano /etc/systemd/system/bench-bridge.service, paste, save; thensudo systemctl daemon-reloadso systemd rereads its catalogue. - Start it once, by hand.
sudo systemctl start bench-bridge, then the Week 9 pair of eyes:systemctl status bench-bridgefor the headline,journalctl -u bench-bridge -ffor the live log, yourprintlines included. - Prove the ownership transfer. Close the SSH session entirely, reopen it,
curl http://localhost:5000/api/light. Fresh JSON from a program nobody is holding: that is the point, observed. - Enable, then the real test.
sudo systemctl enable bench-bridge,sudo reboot, wait, and load the dashboard from your laptop without SSHing in at all. A Pi that serves your project from cold power is a Pi you can carry to a demo.
Keep a copy of the unit file in the repo under deploy/, with the install commands in a comment. The file in /etc/systemd/system/ is configuration, and Week 3's lesson about configuration has not aged: if it is not in the repo, it does not survive a re-image.
Honest limits
Week 11's honesty note still binds: the Flask dev server inside your service is fine for a bench and a classroom demo, not for the open internet, and a service restarting your program does not fix the reasons it crashes, it only buys you the log that says why (journalctl keeps the stack trace of every death). Note also what the ladder's higher rungs would be, since the colleague-run editions of this course deployed whole projects this way: one unit per program, node excepted (the ESP32 needs no systemd; main.py at boot has been its service since Week 8), a status topic so services report into MQTT itself, and log rotation for the long-running. For D5, the single unit you wrote today is the difference between a demo and a prayer.
Checklist for this stage
Check yourself
Why does ExecStart name the venv's python explicitly instead of relying on an activated environment?
source venv/bin/activate edits the current shell's PATH so that python resolves to the venv's interpreter. A systemd service has no login shell and no activation step, so a bare python would resolve to the system interpreter, which, per Week 4's PEP 668 lesson, does not have your packages, and the service dies at its first import. Naming /home/username/project/venv/bin/python skips the convenience and uses the mechanism directly: that file is the venv's interpreter, packages and all.Restart=on-failure brings your bridge back in 2 seconds, forever. Construct the case where this makes a bug harder to notice, and the habit that defends against it.
journalctl -u bench-bridge shows every death with its stack trace, and a periodic glance (or noticing the same trace repeated) turns the invisible crash-loop into a diagnosis. Restart buys availability; only reading buys correctness.Your service shows active (running) but curl gets connection refused on port 5000. Which segment does that pair of facts acquit, and where do you probe next?
journalctl -u bench-bridge -n 30. You are looking for Flask's startup banner with its address line; a missing banner or a traceback locates the fault inside the program, while a banner showing 127.0.0.1 versus 0.0.0.0 replays Week 11's bind-address wall, now one layer deeper.