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, git or fail2ban.
  • 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:

json
{"plan":"budget","image":"debian-12","hostname":"web-1","sshKeyIds":["key_…"],"packages":["htop","git","fail2ban"]}

With the CLI, repeat --package:

bash
vps9 servers create --plan budget --image debian-12 --hostname web-1 --key key_… --package htop --package git

Anything 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:

bash
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:

bash
vps9 servers create --plan budget --image debian-12 --hostname web-1 --key key_… --setup-script ./setup.sh

Example: 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.

bash
#!/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 sshd

Afterwards 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.

bash
#!/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 app

If 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:

bash
tail -F /var/log/vps9-setup.log

Your 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:

bash
cat /var/lib/vps9-setup.exit

0 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.

Be first when orders open

Join the waitlist and we'll send you one email when VPSNine launches. Pick a plan if you already know which one you want, and we'll size the first servers around it.

  • One email, on launch day
  • Monthly, or a year up front in Bitcoin
  • 14-day money-back guarantee