Startup scripts and secrets: set up a server at its first boot
Give an instance a shell script or cloud-config that runs once at its first boot, pass it environment variables and encrypted secrets through /etc/nextify/env, sort instances with labels, and read the setup log to see how it went.
A startup script sets up a new server for you: it installs your packages, writes your configuration and starts your app at the server's first boot, so the instance is ready when you first log in, or without you ever logging in. Passwords, API keys and other secrets go in separately, as environment variables: Nextify stores secret values encrypted and hands them only to the server itself.
This article covers:
- Adding a startup script
- Environment variables and secrets
- Labels
- The setup log
- Changing things later
- Doing it through the API
1. Add a startup script
- In the portal, open Instances → Create instance and choose the region, plan, operating system and SSH keys as usual.
- Open 6. Startup script, labels and environment.
- Under Startup script, type or paste your script, or pick one from Start from an example.
- Click Create instance.
The script can be either of these:
- A shell script: the first line starts with
#!, for example#!/bin/bash. - A cloud-config: the first line is
#cloud-config, followed by cloud-init's YAML (packages, files, users, commands). Your SSH keys and the server's network settings are still applied: your lists are added to Nextify's, never replace them.
Anything else is refused. A script can be up to 64 KiB.
What happens:
- The script runs once, at the first boot, as root, after the network is up. Restarting the server does not run it again.
- If you set environment variables (section 2), they are already in
/etc/nextify/envwhen the script starts. - Everything the script prints goes to
/var/log/cloud-init-output.login the server, and you can read it in the portal (section 4). - The script is stored as you wrote it, and anyone in your account can read it on the instance's page. Never put a password or key in the script itself: use a secret.
- An instance restored from a snapshot does not run a startup script: the snapshot's own setup already ran.
A first example, for Ubuntu and Debian:
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y nginx
echo "<h1>Hello from $(hostname)</h1>" > /var/www/html/index.html
systemctl enable --now nginx
Your account's default security group lets in traffic only from your own instances. To reach this web server from the internet, allow port 80 in a security group (Network → Security groups) attached to the instance.
2. Environment variables and secrets
Under Environment & secrets on the same page, add one row per variable: a name such as API_KEY (upper-case letters, digits and _) and its value. Tick Secret for anything sensitive. Up to 64 variables, each value up to 4 KiB.
Nextify writes them into the server as the file /etc/nextify/env, readable by root only, one line per variable:
API_KEY='s3cr3t-value'
APP_ENV='production'
NEXTIFY_INSTANCE_ID='6f0c2f4e-...'
NEXTIFY_INSTANCE_NAME='web-1'
NEXTIFY_REGION='us-west-1'
The three NEXTIFY_ variables are always there; names starting with NEXTIFY_ are reserved for them.
Use the file:
- In your startup script:
. /etc/nextify/envreads the variables into the script. To pass them on to programs the script starts, useset -a; . /etc/nextify/env; set +a. - In a systemd service:
EnvironmentFile=/etc/nextify/envin the[Service]section gives the service every variable, even when it runs as another user.
What "secret" means:
- A secret's value is stored encrypted, never in the instance's settings, and never written to logs.
- After you create the instance, the value is never shown again, in the portal or the API. The instance's page shows only a hint, for example
••••abcd(the last 4 characters, for values of 8 characters or more). You can replace a secret; you cannot read it back. Keep your own copy where you keep your other secrets. - Inside the server, the value is in
/etc/nextify/envin plain text, readable by root. Anyone with root on the server can read it.
Plain variables (Secret not ticked) are shown on the instance's page and returned by the API.
An example: a Node.js app from a public Git repository, run as a service that gets its settings from the file. Add PORT (for example 3000) and your app's secrets under Environment & secrets:
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y git nodejs npm
git clone --depth 1 https://github.com/your-name/your-app.git /opt/app
cd /opt/app
npm ci --omit=dev
useradd --system --home /opt/app app || true
chown -R app:app /opt/app
cat > /etc/systemd/system/app.service <<'EOF'
[Unit]
Description=My Node.js app
After=network-online.target
[Service]
User=app
WorkingDirectory=/opt/app
EnvironmentFile=/etc/nextify/env
ExecStart=/usr/bin/npm start
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now app
3. Labels
Labels are key=value pairs of your own, for example env=production, team=web or app=shop, up to 32 per instance. Keys use lowercase letters, digits and . _ / -; values are up to 63 characters (letters with accents and other non-ASCII characters count as two or more).
- Add them under Labels when you create the instance, or later on the instance's Setup & environment tab.
- The Instances list shows them under each name. Type
env=production(or justteamfor "has a team label") in Filter by label, or click a label, to see only the matching instances. Several entries must all match. - In the API:
GET /v1/instances?label=env=production&label=team.
Labels are only for you: they never change networking. Network firewalls use tags, which you set separately.
4. The setup log
On the instance's page, open Setup & environment and scroll to Setup log. It shows:
- the status of the first-boot setup: Running (not finished yet), Finished or Failed;
- the last 64 KiB of
/var/log/cloud-init-output.log, which includes everything your script printed.
The log is read from the server while it runs. While the setup is still running, the page reads it again every few seconds; click Refresh to read it at any time. On a stopped instance the page shows the copy read last (or nothing, if it was never read): power the instance on to read it again.
If the setup failed:
- Read the end of the log: the failing command's error is usually the last thing printed.
set -euo pipefailat the top of a shell script stops it at the first failing command, which makes this easier. - A script saved with Windows line endings fails at its first line (
/bin/bash^M: bad interpreter). Save it with Unix (LF) line endings. - On the server itself,
cloud-init status --longand/var/log/cloud-init.loghave more detail. Use Launch console if SSH is not working.
5. Changing things later
- The startup script cannot be changed: it ran at the first boot. You can read it on the Setup & environment tab (Show script). To run something new, use the console or SSH, or create a new instance with the new script.
- Environment & secrets: click Edit, change or add rows, and click Save variables. A secret you do not replace keeps its value (it shows as "kept as it is"); click Replace value to give it a new one. Removing a row removes the variable. A running server gets the new
/etc/nextify/envwithin about a minute; a stopped one when it next starts. Programs that already read the file, such as a running service, see the change after they restart (systemctl restart app). - Labels: click Edit labels, change them, and click Save labels.
6. Through the API
POST /v1/instances takes user_data (the script), labels (an object) and env (a list of {name, value, secret}):
jq -n --rawfile ud setup.sh --arg key "$MY_API_KEY" '{
name: "web-1", machine_type: "sm1.s1-1", region: "us-west-1", image: "ubuntu-24.04",
user_data: $ud, labels: {env: "staging"},
env: [{name: "API_KEY", value: $key, secret: true}, {name: "APP_ENV", value: "staging"}]
}' | curl -sS -X POST https://api.nextifynow.com/v1/instances \
-H "Authorization: Bearer $NEXTIFY_TOKEN" -H "Content-Type: application/json" -d @-
The other routes: GET /v1/instances/{id}/user-data (the script), GET and PUT /v1/instances/{id}/env (the variables; on PUT, {"name": "API_KEY", "secret": true} without a value keeps that secret), PUT /v1/instances/{id}/labels and GET /v1/instances/{id}/setup-log. The API documentation has a complete example.
Good to know
- The script runs as root with no one watching: make it non-interactive (
DEBIAN_FRONTEND=noninteractive,apt-get install -y). - It runs after the network is up, but a package mirror or Git host can still be slow or briefly unreachable. Retry downloads that matter, for example
curl --retry 5. - A secret in
/etc/nextify/envis as safe as root on your server. Keep SSH keys and access to your account under control. - Creating many similar instances? Keep the script in a file and send it through the API or Terraform, so every server is set up the same way.