Skip to content
Murali Krishnan A
All writing

21 Sept 2026

Jellyfin from scratch, part 5: making it survive a reboot

The last part of the Jellyfin series. Turning the bot script into a systemd service, keeping the local Telegram API server alive with Docker's own restart policy, log rotation, and a short security checklist for the whole setup.

4 min read · jellyfin, selfhosted, telegram, beginners

This is the last part of a five-part guide to running Jellyfin on your own server and connecting it to a Telegram bot. Parts 1 and 2 set up Jellyfin itself. 3 and 4 built the bot. This part makes all of it survive being left alone, which is the actual point of running it on a server instead of a laptop.

Everything in part 4 worked, right up until I closed the SSH session and the bot stopped answering. A script running in a terminal dies with that terminal. Getting it to survive a reboot, a crash, or an instance restart - and to leave a trail when something does go wrong - is three small pieces of configuration, not a rewrite.

The bot as a systemd service

Jellyfin already runs as a systemd service; you saw that in part 1 with systemctl status jellyfin. Give the bot script the same treatment.

# /etc/systemd/system/telegram-jellyfin-bot.service
[Unit]
Description=Telegram to Jellyfin upload bot
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/telegram-bot
ExecStart=/home/ubuntu/telegram-bot/.venv/bin/python3 bot.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

A few of these lines are doing real work:

sudo systemctl daemon-reload
sudo systemctl enable --now telegram-jellyfin-bot
sudo systemctl status telegram-jellyfin-bot

enable makes it start on boot. --now also starts it immediately, so you get one command instead of two.

Logs

Once it's a systemd service, its output goes to the system journal automatically

journalctl -u telegram-jellyfin-bot -f

-f follows it live, the same way tail -f would follow a log file. Send the bot a file from another window and watch the same print() output from part 4 show up here instead of in a terminal you have to keep open.

Keeping the local Telegram server alive

The Docker container from part 3 already had --restart always in its run command, which covers most of this on its own - Docker restarts it if it crashes, and if you enabled the Docker service itself (systemctl enable docker, on by default after apt install docker.io), it comes back after a reboot too.

Confirm both:

$ systemctl is-enabled docker
enabled

$ docker inspect telegram-bot-api --format '{{.HostConfig.RestartPolicy.Name}}'
always

If the second one doesn't say always, the container was started without that flag at some point - remove it and re-run the docker run command from part 3 with --restart always included.

Log rotation

The Docker container's own logs, and the systemd journal for the bot, both grow without bound by default on a fresh Ubuntu install. On a 60 GB instance that matters eventually, not immediately, but it's a five-minute fix now versus a confusing "why is my disk full" later.

For the systemd journal, cap it in /etc/systemd/journald.conf:

SystemMaxUse=200M
sudo systemctl restart systemd-journald

For the Docker container, cap its log driver in the docker run command from part 3 by adding:

--log-opt max-size=10m --log-opt max-file=3

A short security checklist

Pulling together what's been mentioned across the series, in one place:

Where this actually leaves you

A file sent from a phone, anywhere, over Telegram, shows up as a playable title in Jellyfin within a few seconds, on a server that costs about as much as a single movie ticket a month, with nothing running in a terminal you have to keep open. That was the whole goal five parts ago.

The parts worth remembering if you're adapting this rather than copying it directly: the 20 MB Telegram limit isn't a bug to work around cleverly, it's a sign you want your own copy of the Bot API server; a bind mount is what makes a container's files visible to a plain Python script running next to it, not inside it; and almost every "it worked in testing" failure in this series traced back to a permissions boundary - Lightsail's firewall in part 1, file ownership in part 4 - that a quick local test never crosses but a real file, arriving for real, always does.