I needed one machine to connect to three different frp servers. Most tutorials solve this by copying frpc.service three times and editing the path in each. Change one parameter and you have to remember to change it in three files — that breaks eventually.
systemd has a built-in answer: template units. Write frpc@.service once, then spin up as many instances as you like. This post walks through the full frp setup first, then shows how the same pattern applies to xray, socat, Python scripts, or anything else you need to run several copies of.
What a template unit is
A normal unit file is called frpc.service. A template unit has an @ in the name: frpc@.service.
The template itself never runs. When you start frpc@azure_hk.service, systemd uses the template to generate a real instance, taking the string after the @ (here azure_hk) as the instance name and substituting it wherever %i appears in the unit file.
So: one template + N instance names = N independent services. Each instance gets its own process, its own logs and its own start/stop state, while sharing a single unit file.
The frp setup
1. Write the template unit
Create /etc/systemd/system/frpc@.service:
[Unit]
Description=Frp Client Service - %i
After=network.target syslog.target
Wants=network.target
[Service]
Type=simple
Restart=on-failure
RestartSec=5s
# %i is replaced by whatever follows the @
ExecStart=/usr/local/frp/frpc -c /usr/local/frp/frpc-%i.toml
[Install]
WantedBy=multi-user.target
2. Name config files to match
The whole thing hinges on config file names matching instance names. The convention here is /usr/local/frp/frpc-<instance>.toml:
frpc-azure_hk.toml→ instancefrpc@azure_hkfrpc-aliyun.toml→ instancefrpc@aliyunfrpc-vps2.toml→ instancefrpc@vps2
Adding a new tunnel later means dropping in one config file — you never touch the service file again.
3. Start them
sudo systemctl daemon-reload
sudo systemctl enable --now frpc@azure_hk
sudo systemctl enable --now frpc@aliyun
enable --now is enable (start at boot) plus start (start right now) in one go.
4. Manage them all at once
Instance names accept wildcards, so one command covers every instance. Quote them — otherwise the shell expands * against the current directory first:
systemctl status "frpc@*"
sudo systemctl restart "frpc@*"
sudo systemctl stop "frpc@*"
Same for logs, with -f to follow:
journalctl -u "frpc@*" -f
Or just one instance:
journalctl -u frpc@azure_hk -n 100 --no-pager
Applying this to anything else
Nothing above is specific to frp. Any program that runs several copies of the same binary with different arguments or configs is a candidate.
Available specifiers
%i isn’t the only placeholder. The ones I actually use:
| Specifier | Meaning | Value in frpc@azure_hk.service |
|---|---|---|
%i | Instance name, as-is (escaped form) | azure_hk |
%I | Instance name, unescaped (- becomes /) | azure_hk |
%p | Prefix — the part before the @ | frpc |
%n | Full unit name | frpc@azure_hk.service |
%N | Unit name without the suffix | frpc@azure_hk |
%h | Home directory of the running user | /root |
%u | User name | root |
%i and %I only differ once the instance name contains special characters. An instance name can’t contain / directly — you use - instead, and %I turns it back. If you need to derive an instance name from a path, run it through systemd-escape:
systemd-escape -p /srv/app/site-a
Example 1: xray / v2ray with several configs
[Service]
ExecStart=/usr/local/bin/xray run -c /usr/local/etc/xray/%i.json
Put the configs in /usr/local/etc/xray/hk.json and tokyo.json, then systemctl enable --now xray@hk xray@tokyo.
Example 2: several socat port forwards
Here the parameters live on the command line rather than in a config file. The trick is one environment file per instance:
/etc/systemd/system/socat-fwd@.service
[Unit]
Description=socat forward - %i
After=network.target
[Service]
Type=simple
Restart=on-failure
RestartSec=5s
EnvironmentFile=/etc/socat-fwd/%i.conf
ExecStart=/usr/bin/socat TCP4-LISTEN:${LPORT},fork,reuseaddr TCP4:${RHOST}:${RPORT}
[Install]
WantedBy=multi-user.target
/etc/socat-fwd/nas.conf
LPORT=8080
RHOST=192.168.1.50
RPORT=5000
Then sudo systemctl enable --now socat-fwd@nas. This pairs well with my port forwarding post: that one covers how to write a single forward, this one solves what happens once you have a dozen of them.
Tip:
EnvironmentFile=-/etc/socat-fwd/%i.conf— the leading-means “don’t fail if the file is missing”. Handy when the environment file is an optional override.
Example 3: multiple Python scripts or bots
[Service]
Type=simple
User=vinkey
WorkingDirectory=/opt/bots/%i
ExecStart=/opt/bots/%i/.venv/bin/python main.py
Restart=on-failure
RestartSec=10s
One directory and one virtualenv per bot; bot@alpha and bot@beta run completely independently.
Things that trip people up
Always daemon-reload after editing the template. Changes don’t apply automatically, and running instances need a restart to pick them up:
sudo systemctl daemon-reload && sudo systemctl restart "frpc@*"
You can’t enable the template itself. systemctl enable frpc@.service fails, because systemd doesn’t know which instance you mean. If you do want a default for when no instance is given, add a line to [Install]:
[Install]
WantedBy=multi-user.target
DefaultInstance=azure_hk
Special-casing one instance. If a single instance needs different settings, don’t copy the unit file — generate a drop-in override:
sudo systemctl edit frpc@aliyun
That creates /etc/systemd/system/frpc@aliyun.service.d/override.conf, where you write only the lines you’re changing; everything else still comes from the template.
Don’t forget the quotes. systemctl stop frpc@* can be mangled by shell globbing if a matching file happens to exist in the current directory. systemctl stop "frpc@*" is the safe form.
List the instances currently defined:
systemctl list-units "frpc@*" --all