A headless Raspberry Pi can start FFmpeg automatically at boot by running it as a systemd service. The reliable sequence is to make the command work interactively, place shell logic in a wrapper when needed, test the service, and only then enable it for future boots.
The important detail is that systemd's ExecStart is not a shell command line. A pipeline, output redirection, variable expansion, or && chain will not behave as it does in Bash unless you deliberately invoke a shell or, more safely, put that logic in an executable script.
Prepare the Pi for headless operation
Use a supported Raspberry Pi OS installation on the Pi's boot media and configure networking before you place the device somewhere without a monitor. Raspberry Pi recommends Raspberry Pi OS Lite for headless setups and lists 8 GB as a recommended starting storage capacity for that edition. That is a setup recommendation, not a universal requirement for every streaming workload. Check the Raspberry Pi getting started documentation for the current imaging and remote-access process.
During imaging, configure the network and at least one way to administer the Pi remotely, such as SSH or Raspberry Pi Connect. Test that access before working on the stream. If the Pi is in another room, this is more than a convenience: a service can keep running without a desktop session, but you still need a way to inspect it when the source disappears or a boot change fails.
Update the system using the maintenance process appropriate to your Raspberry Pi OS installation, then identify the account that should own the stream. An ordinary user is usually a better starting point than root. The service may need access to a camera, USB audio device, or another capture source, so check the relevant device permissions rather than assuming the choice of user is harmless.
Do not begin with boot automation. First decide what the real input is: a local file, a camera, an ALSA audio device, a network source, or a producer process feeding FFmpeg. FFmpeg's command structure differs between those cases. Its official command-line documentation describes the general arrangement of global options, input options, inputs, output options, and outputs. The exact command must match your source and the output configuration you have verified separately.
Find the installed FFmpeg
Check whether FFmpeg is already available and where the executable is located:
command -v ffmpeg
ffmpeg -version
command -v may return /usr/bin/ffmpeg, but do not copy that path blindly from another Pi. Packages, manually installed builds, and user-local installations can place the executable elsewhere. Use the path returned on the actual device. You can also inspect the locally installed documentation with commands such as man ffmpeg; the FFmpeg documentation index notes that the online documentation may not describe an older build installed on your system exactly.
Before proceeding, run the complete, verified FFmpeg invocation manually. Confirm that it can open the real input and that it exits with a useful error when the input is unavailable. Keep any stream credential private while testing and never paste a real key into an article, repository, screenshot, or support request.
Decide whether you need a wrapper script
A direct systemd service is suitable when one FFmpeg process is all that is required. In that case, ExecStart can point directly to the executable followed by its arguments. Use absolute paths where practical so the service does not depend on the interactive shell's PATH.
A wrapper is the safer choice when you need shell syntax or more than one preparation step. Typical examples include starting a camera producer and piping its output into FFmpeg, creating a temporary directory, selecting an input based on a condition, or exporting environment variables before launch. Put those actions in a separate executable file and let systemd supervise the script.
For example, create a private directory for the service account and save a script such as /home/streamer/bin/start-stream.sh:
#./bin/sh
set -eu
FFMPEG=/usr/bin/ffmpeg
# Replace this with the complete command tested interactively.
exec "$FFMPEG" \
YOUR_VERIFIED_INPUT_AND_OUTPUT_OPTIONS
The placeholder is intentional. This article does not establish a current YouTube ingest URL, stream-key workflow, or encoder setting. Confirm those details in YouTube's current official live streaming documentation before completing the command. A YouTube stream key is a credential, so keep it out of public examples and handle it according to the current account documentation.
Make the script executable and confirm that its ownership is appropriate:
chmod 750 /home/streamer/bin/start-stream.sh
chown streamer:streamer /home/streamer/bin/start-stream.sh
The exec in the example replaces the wrapper process with FFmpeg. That gives systemd the actual FFmpeg process to supervise and allows FFmpeg's exit status to reach systemd. If your script deliberately starts several processes, waits for one, or performs cleanup, write and test that behaviour explicitly rather than assuming the service manager will infer it.
If you use a pipeline, keep it inside the wrapper:
producer-command | /usr/bin/ffmpeg YOUR_VERIFIED_OPTIONS
This is only a structural example. It still needs a real producer, real input and output options, correct quoting, and a decision about which process should determine failure. A pipeline that hides an early producer failure can make a service appear healthier than the stream really is, so inspect the exit behaviour before relying on it overnight.
Create the systemd service
Save a unit under /etc/systemd/system/ffmpeg-stream.service. Start with a simple unit and adapt it to the installed systemd version and the actual network manager on the Pi:
[Unit]
Description=FFmpeg live stream
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=streamer
WorkingDirectory=/home/streamer
ExecStart=/home/streamer/bin/start-stream.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
The names and paths are examples. Replace streamer, the working directory, and the wrapper path with values that exist on your Pi. If you are using a direct FFmpeg invocation, ExecStart might instead look like this:
ExecStart=/usr/bin/ffmpeg YOUR_VERIFIED_INPUT_AND_OUTPUT_OPTIONS
Do not write this as though systemd were running Bash:
ExecStart=/bin/ffmpeg ... | tee /var/log/stream.log
Do not expect redirection such as >, expansion such as $HOME, or a chain such as command-one && command-two to be interpreted by ExecStart as shell syntax. Use a wrapper script, or explicitly invoke a shell only when you understand the quoting, exit-status, and secret-handling consequences. A wrapper is generally easier to test and review.
User= should name an unprivileged account that can read the input and execute the script. If the source is a camera or USB device, the account may need membership of a device-access group. Check the device's ownership and permissions on the Pi and add only the access that is required. Running the whole stream as root can hide a permissions mistake rather than solving it.
The network lines deserve careful interpretation. network-online.target is an ordering and dependency mechanism, not proof that DNS works, the Pi has an internet route, or the YouTube endpoint is reachable. Its usefulness depends on the network manager and its wait-online service being configured on that installation. Check the local systemd documentation and the network manager in use instead of treating a familiar unit fragment as a guarantee.
A restart policy can be useful when FFmpeg exits because of a temporary process failure. Restart=on-failure does not repair a wrong key, an unavailable camera, an overloaded Pi, a failed DNS lookup, or a damaged source file. RestartSec=5 is an example delay, not a promise that five seconds is suitable for every source. Repeated restarts should lead you to the logs and the underlying failure, not to an assumption that the service is healthy.
Check paths, environment, and capture access
A service does not inherit your interactive login in the same way as your terminal. It may have a different PATH, HOME, current directory, locale, device access, and set of environment variables. This is why a command that works over SSH can fail when systemd launches it.
Check every path used by the wrapper and the FFmpeg command. Use full paths for executables, input files, helper programs, and directories where possible. If a relative path is necessary, set WorkingDirectory= deliberately and confirm that the service user can read or write there.
If the command needs environment variables, define them intentionally. For a non-secret value, an Environment= line may be enough. For a stream credential, avoid placing the value directly in a unit file that can be copied or displayed. A root-owned environment file with restrictive permissions, or another secret-handling method suitable for the installation, is preferable. Make sure the wrapper does not print the secret when it starts and that diagnostic output cannot expose it.
You can test the service account's access without launching the full stream. For example, inspect the source device and file as that account, and run the wrapper in the same account when the test is safe:
sudo -u streamer test -r /path/to/input-file
sudo -u streamer /home/streamer/bin/start-stream.sh
Stop the process cleanly after confirming the input opens. For a camera, compare the device permissions and group memberships between your login account and streamer. For audio, check the active device and the access policy used by the installed audio stack. Capture access is an installation-specific issue, so a service file copied from another model or operating system may not fit your Pi.
Keep power and storage in the check as well. A small board that can read a file may still struggle with a demanding real-time conversion. A storage device can fill if you deliberately log output to a file without rotation. Sending normal service output to the journal is often simpler, but inspect its volume and retention settings before leaving a noisy process unattended.
Start manually and inspect the logs
Reload the unit definition after creating or editing the file:
sudo systemctl daemon-reload
Do not enable boot startup yet. Start the unit manually so that failures are easy to reproduce while you still have an SSH session open:
sudo systemctl start ffmpeg-stream.service
sudo systemctl status ffmpeg-stream.service --no-pager
The status output gives you the service state, recent messages, process information, and often the first useful error. For the complete boot-session journal, use:
sudo journalctl -u ffmpeg-stream.service -b
To follow new messages while the service runs:
sudo journalctl -u ffmpeg-stream.service -f
A service that shows active (running) only proves that systemd has a process matching the unit's current state. It does not prove that FFmpeg opened the intended source, that the remote ingest accepted the connection, or that viewers can see the public live event. Check FFmpeg's own diagnostic output and the current YouTube Live control-room state separately.
If the unit exits immediately, inspect the status and journal first. Common causes are a wrong executable path, a missing wrapper permission, an invalid working directory, a command that only works with your login environment, or a source device that is not ready. Run the same tested command as the service user and compare the result rather than adding random delays.
If it starts and then restarts repeatedly, look for the first meaningful FFmpeg error, not just the later systemd restart messages. Check source availability, CPU load, network reachability, credentials, and the command's exit status. Automatic retries can make a temporary failure recover, but they can also make a bad configuration produce a long, repetitive journal.
For stream-specific symptoms, the stream health troubleshooting guide is useful after the local service itself is understood. It helps separate a Pi process problem from an ingest or live-control-room problem.
Enable boot startup only after testing
Once the service starts correctly by hand, reload the unit one more time after any final edit and enable it for future boots:
sudo systemctl daemon-reload
sudo systemctl enable ffmpeg-stream.service
enable arranges for the unit to be started through its [Install] relationship. It does not, by itself, start the current instance. To enable and start in one command, use sudo systemctl enable --now ffmpeg-stream.service, but only after the separate manual test has passed.
Keep the unit name exact and check the result:
systemctl is-enabled ffmpeg-stream.service
systemctl status ffmpeg-stream.service --no-pager
If you change the unit later, run daemon-reload again. If you change the wrapper, check its executable bit and ownership again. If you change the input device, account, or network configuration, repeat the relevant manual test instead of assuming the previous result still applies.
A desktop-session autostart entry is another approach, but it needs a graphical session and a user login. It can make sense when the stream is intentionally tied to a desktop application. For an unattended Pi with no monitor, a system service is usually clearer because its lifecycle, user, dependencies, and logs are explicit.
For a direct comparison:
| Approach | Best fit | Main trade-off |
|---|---|---|
| Direct FFmpeg in systemd | One FFmpeg process with no shell pipeline | Fewer moving parts, but every argument and path must be correct in the unit |
| Wrapper launched by systemd | Pipelines, preparation steps, or environment setup | Easier shell logic, but quoting, permissions, exit behaviour, and secret handling need care |
| Desktop-session autostart | A stream attached to a graphical login | Depends on the desktop session and login, so it is less suitable for an unattended headless Pi |
If maintaining a Pi becomes more work than maintaining the channel, a hosted workflow can remove the need to keep local power, networking, capture access, and boot supervision healthy. StreamNeo removes that particular always-on computer and restart burden by taking an uploaded video and running the YouTube broadcast after you provide the channel connection details, while your own computer can remain switched off.
Reboot and verify the complete path
Test an actual reboot before calling the setup complete. A service that starts from an already-running terminal can still fail during boot because the network, camera, USB audio device, filesystem, or wait-online mechanism is not ready at the same time.
Before rebooting, record the commands you will use to inspect the result:
systemctl is-enabled ffmpeg-stream.service
systemctl status ffmpeg-stream.service --no-pager
sudo journalctl -u ffmpeg-stream.service -b
Then reboot through your remote session:
sudo reboot
Reconnect after the Pi returns and check whether the service is enabled and active. Inspect the journal for the current boot, not only an older successful test. If the service failed early, compare its start time with the network and device messages. A delay can sometimes be appropriate, but first establish which dependency was actually late.
Verify the full chain in stages. Confirm that the process is running, that FFmpeg reports the expected input and output behaviour, and that the YouTube Live control room shows the current connection or ingest state. The public viewing page is a separate check. A running local process is not evidence that the broadcast is visible to viewers.
After a successful reboot, test a failure you can safely reproduce. Disconnecting a source or stopping FFmpeg may show whether the restart policy behaves as intended, but do not perform this while viewers depend on the stream unless you have a maintenance plan. Observe the journal and then restore the source. Repeated retries should remain understandable rather than silently masking a broken setup.
Keep a short record of the working service account, executable path, input path, unit name, device permissions, and recovery commands. Do not include the stream key in that record. If someone else maintains the channel, this small handover note is more useful than a copied unit file with unexplained placeholders.
If the stream is a looped video, also review the channel's content and rights separately. The guidance on 24/7 looped streaming and YouTube policy covers a different risk from the Pi service itself. For music channels, the music licensing guide addresses ownership and permission questions that systemd cannot solve.
Before committing, compare the operating options on the pricing page. When the file and channel are ready, start free — 24-hour trial, no card.
FAQ
Does systemd run ExecStart through Bash?
No. ExecStart normally starts the specified executable with arguments; it does not interpret ordinary shell pipelines, redirection, variable expansion, or command chaining as Bash would. Put shell logic in an executable wrapper, or invoke a shell deliberately with careful quoting.
Should the FFmpeg service run as root?
Usually, start with a dedicated unprivileged user. Give that account access to the input file, capture device, directories, and executable that it actually needs. Running as root can conceal a device-permission problem and gives a streaming process more authority than necessary.
Does active (running) prove that YouTube is receiving the stream?
No. It confirms the local service process is running according to systemd. You must also check FFmpeg's output, network reachability, the current YouTube Live control-room state, and the public viewing result.
Will Restart=on-failure keep the channel live?
It can restart FFmpeg after a process failure, but it cannot correct an invalid credential, missing input, overloaded hardware, unavailable network, or rejected ingest connection. Treat repeated restarts as a diagnostic signal and inspect the journal before changing the policy.