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

Source: https://rack4.io/help/setup-scripts · Updated: 2026-11-01

A setup script is a shell script you add to a Rack4 order that runs once, as root, on the server's first boot; extra packages are installed just before it. Use them so a new server comes up with your user, your tools and your services already in place, from the panel, the API or the CLI.

When you order a server, you can ask for two things on top of the distribution and an optional [one-click app](https://rack4.io/help/one-click-apps):

- **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 are part of every order, in the panel and the API. 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](https://rack4.io/help/install-the-rack4-cli), repeat `--package`:

```bash
rack4 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 $RACK4_TOKEN" -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" --data @- "$API/servers"
```

With the CLI, point at the file:

```bash
rack4 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](https://rack4.io/help/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.

## Ready-made recipes

Tested scripts for common jobs, each with its full text, what it changes and how to check it. Every one works on Ubuntu, Debian, AlmaLinux, Rocky Linux and Fedora, and has a **Deploy with Rack4** link that opens the order form with the script already in the field, for you to read and adjust before ordering:

- [Secure baseline](https://rack4.io/help/secure-server-setup-script): a sudo user, SSH with keys only, a host firewall, fail2ban and automatic security updates.
- [Docker host](https://rack4.io/help/docker-host-setup-script): Docker Engine and Compose from Docker's repository, with container logs capped.
- [Tailscale](https://rack4.io/help/tailscale-setup-script): Tailscale started at boot, joined to your tailnet if you give it a key.
- [Caddy reverse proxy](https://rack4.io/help/caddy-reverse-proxy-setup-script): HTTPS for your domain in front of an app on the server.

To put a deploy link in your own README, use the link from a recipe's page, for example:

```markdown
[Deploy with Rack4](https://my.rack4.io/servers/new?recipe=docker-host)
```

The link only works for these recipes: it can't carry a script of your own. For that, share the script and have people paste it into the form.

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

## Troubleshooting

**`invalid_packages`: "Web-Server" is not a package name: lowercase letters, digits and + . _ - only, separated by spaces or commas.** A name has a capital letter, a space inside it or another character. Package names are always lowercase.

**`invalid_setup_script`: "The setup script can be at most 16 KiB."** Move the bulk into a file the script downloads (from your own repository, over HTTPS), and keep the script itself to the steps that fetch and run it.

**The packages you listed aren't installed.** One name didn't exist, so the package manager refused the whole list. `/var/log/cloud-init-output.log` names the one it couldn't find.

**`/var/lib/vps9-setup.exit` holds `127`.** A command in the script wasn't found: usually a tool from a package you didn't list, or a command that has another name on this distribution.

## Next steps

- The ready-made recipes: [secure baseline](https://rack4.io/help/secure-server-setup-script), [Docker host](https://rack4.io/help/docker-host-setup-script), [Tailscale](https://rack4.io/help/tailscale-setup-script) and [Caddy reverse proxy](https://rack4.io/help/caddy-reverse-proxy-setup-script).
- [Create and destroy preview servers per pull request](https://rack4.io/tutorials/preview-servers-per-pull-request), which orders servers with a setup script from CI.
- [Manage Rack4 servers with Ansible](https://rack4.io/tutorials/manage-rack4-servers-with-ansible), for changes after the first boot.
- [Compare the plans](https://rack4.io/plans) to pick the size for your script's workload.
