Extra packages and setup scripts
Name extra packages and a shell script for a new server's first boot, so it comes up with your user, your tools and your services already in place.
When you order a server, you can ask for two things on top of the distribution and an optional one-click app:
- Extra packages: up to 20 package names from the distribution's own repositories, such as
htop,gitorfail2ban. - A setup script: a shell script of up to 16 KiB that runs once, as root, on the first boot, after the packages are installed.
Both open with orders at launch. Together they mean the server is set up the way you like the first time you log in, instead of after twenty minutes of typing.
Extra packages
Give the names exactly as the distribution's package manager knows them: lower case, letters, digits and + . _ -. The same tool can have different names on different families: netcat-openbsd on Debian is nmap-ncat on AlmaLinux. Check every name: if one doesn't exist, the package manager refuses the whole list, so none of the packages are installed, the app's own included. /var/log/cloud-init-output.log names the one it couldn't find.
In the panel, type them into the order form, separated by spaces. With the API, send a list:
{"plan":"budget","image":"debian-12","hostname":"web-1","sshKeyIds":["key_…"],"packages":["htop","git","fail2ban"]}With the CLI, repeat --package:
vps9 servers create --plan budget --image debian-12 --hostname web-1 --key key_… --package htop --package gitAnything from outside the distribution, like a vendor's own repository, belongs in a setup script.
Setup scripts
The script is run by the server itself, not by us. Start it with a #! line, usually #!/bin/bash, and add set -euo pipefail so it stops at the first command that fails instead of carrying on regardless.
In the panel, paste it into the order form. With the API, send it as a string in setupScript; jq handles the quoting:
jq -n --rawfile s setup.sh '{plan:"budget", image:"debian-12", hostname:"web-1", sshKeyIds:["key_…"], setupScript:$s}' |
curl -s -X POST -H "Authorization: Bearer $VPS9_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" --data @- "$API/servers"With the CLI, point at the file:
vps9 servers create --plan budget --image debian-12 --hostname web-1 --key key_… --setup-script ./setup.shExample: a sudo user, and no root over SSH
This copies root's SSH keys to a new user called deploy, gives it sudo, and then stops root logging in over SSH. Change the name to suit.
#!/bin/bash
set -euo pipefail
user=deploy
id "$user" >/dev/null 2>&1 || useradd -m -s /bin/bash "$user"
# sudo without a password, since the account has none
echo "$user ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-$user"
chmod 440 "/etc/sudoers.d/90-$user"
install -d -m 700 -o "$user" -g "$user" "/home/$user/.ssh"
install -m 600 -o "$user" -g "$user" /root/.ssh/authorized_keys "/home/$user/.ssh/authorized_keys"
echo "PermitRootLogin no" > /etc/ssh/sshd_config.d/10-no-root.conf
systemctl reload ssh 2>/dev/null || systemctl reload sshdAfterwards you log in as ssh deploy@203.0.113.42 and use sudo for root. The SSH hardening checklist covers the rest. If you'd rather your user had a sudo password, set one with passwd deploy on your first login and drop NOPASSWD:.
Example: install and start a service
This puts a small app in place and has systemd run it at boot. It works on every distribution we offer.
#!/bin/bash
set -euo pipefail
id app >/dev/null 2>&1 || useradd --system --home /opt/app --shell /usr/sbin/nologin app
install -d -o app -g app /opt/app
curl -fsSL https://example.com/releases/app-linux-amd64 -o /opt/app/app
chmod 755 /opt/app/app
cat > /etc/systemd/system/app.service <<'EOF'
[Unit]
Description=My app
After=network-online.target
Wants=network-online.target
[Service]
User=app
ExecStart=/opt/app/app --listen 127.0.0.1:8080
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now appIf your script relies on an app you picked, Docker say, check that it's there first with command -v docker and fail clearly if not, so the log tells you what happened.
Check the result
The server answers SSH before your script has finished. Log in and watch it:
tail -F /var/log/vps9-setup.logYour script's output goes there, after an app's install if you picked one. The file appears once the packages are in; until then, tail -F waits for it, and the package installs print to /var/log/cloud-init-output.log. When it's done, /var/lib/vps9-setup.exit holds the exit code:
cat /var/lib/vps9-setup.exit0 means it all worked. Anything else means a step failed, and the end of the log shows which. Fix the script, then run it by hand to finish the job, or reinstall to try it on a clean disk.
Write it so it can run twice
The script runs once per install. You'll still end up running it again: by hand after a failure, or on a reinstall. So write it so a second run does no harm. The examples above do that: id … || useradd only adds a missing user, install -d doesn't mind an existing folder, and writing a whole file with > replaces it rather than adding to it. Avoid >> on config files, which adds the same line again every time.
Keep secrets out of it
We store the script encrypted and never show it back, but it is copied onto the server's disk to run: /root/vps9-setup.sh and /root/vps9-user.sh, readable by root only, and cloud-init keeps its own copy of the first-boot configuration under /var/lib/cloud. Anyone with root on the server can read it. Don't put anything in it you can't change later: no long-lived passwords, no private SSH keys, no API keys with wide access.
If the server needs a secret, give it one you can revoke, such as a token scoped to that one server, and replace it after the first login. Or copy the secret over with scp once the server is up.
Reinstalls
A reinstall runs the same packages and setup script again on the fresh system, unless you change them in the reinstall form. That's usually what you want: the server comes back the way you built it. Clear the script in the form if you want a plain system instead.