GCP VM Startup Scripts Explained: How to Use, Debug, and Secure Them
A startup script runs automatically every time a Compute Engine VM boots. Without one, every new VM in your fleet starts as a blank OS and needs manual configuration. With one, VMs install software, pull config, and start services on their own, whether you are creating one instance or a hundred.
This page explains what startup scripts are, how they work, how to pass them to a VM, how to debug them when they fail, and how to use them safely. It also covers when to prefer a custom image instead, and how startup scripts fit into instance templates and managed instance groups.
Simple explanation
A startup script is a shell script attached to the VM’s metadata. Compute Engine reads the metadata and runs the script as root shortly after the OS finishes booting. It runs on every boot, not just the first one. That means if you restart a VM, or if a managed instance group replaces it with a fresh copy, the script runs again.
Common uses: installing a web server, pulling a config file from Cloud Storage, registering the VM with a service registry, or setting up environment variables before an application starts.
Think of a startup script as a note you leave for the VM to follow when it wakes up. Before it starts accepting any work, it reads the note and runs every instruction on it: install these packages, fetch this config file, start this service. The VM does not need anyone logged in to make it happen.
Because the script runs on every boot, you should write actions to be safe
to repeat. This property is called idempotency. Installing
a package with apt-get install -y nginx is idempotent by
default: if nginx is already there, apt does nothing. Creating a file,
seeding a database, or calling an external API to register the VM is not
idempotent unless you add a guard condition first.
How startup scripts work in Compute Engine
When a Compute Engine VM boots, the guest OS starts a service called
google-startup-scripts.service. This systemd unit reads the
instance metadata server at http://metadata.google.internal
and looks for one of two keys:
startup-script— the script text stored directly in metadatastartup-script-url— a Cloud Storage path from which the VM downloads and runs the script
The script executes as root. Both stdout and stderr are captured by the systemd journal and also streamed to the serial console, which is how you read the output when the VM is not yet accessible over SSH.
In a managed instance group, VMs are regularly replaced through rolling updates, autohealing, or scale-in and scale-out events. Every replacement VM runs the startup script from scratch. Scripts must be idempotent and must not depend on state that only the previous VM had.
Ways to pass a startup script
There are three methods. The right one depends on how you are managing your VMs and whether the script changes frequently.
Inline script with metadata
Pass the script text directly on the command line using
—metadata=startup-script=’…’. This is the fastest way to
get started and works well for short scripts.
gcloud compute instances create my-web-server \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--zone=us-central1-a \
--metadata=startup-script='#!/bin/bash
set -e
apt-get update -y
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
echo "Startup complete" > /tmp/startup-done'Add set -e at the top of every bash startup script. Without
it, bash continues past a failed command silently. The VM appears healthy
while the software it needed was never actually installed.
With set -e, failures stop the script immediately and show
up clearly in the serial console log.
Inline scripts are convenient for testing but become unwieldy for anything longer than a few lines. For scripts you want to version-control or reuse, use one of the file-based methods below.
Script from a local file
Keep the script in a file and reference it with
—metadata-from-file. The script lives in source control
alongside your infrastructure code, and the command stays readable.
gcloud compute instances create my-web-server \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--zone=us-central1-a \
--metadata-from-file=startup-script=./startup.shThe file path is local — it exists on the machine running the
gcloud command. The contents are read and stored in the VM’s
metadata at creation time. If you update the file later, existing VMs are
not affected unless you update their metadata explicitly.
Script from Cloud Storage with startup-script-url
Upload the script to a Cloud Storage bucket and reference it with
—metadata=startup-script-url=gs://…. The VM downloads and
runs the script from GCS at each boot.
# Upload the script to Cloud Storage
gcloud storage cp startup.sh gs://my-config-bucket/startup.sh
# Create a VM that fetches the script from GCS at boot
gcloud compute instances create my-web-server \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--zone=us-central1-a \
--metadata=startup-script-url=gs://my-config-bucket/startup.shThis is the recommended approach for scripts shared across multiple VMs or
used in instance templates.
Update the script in Cloud Storage once and every subsequent boot picks up
the new version, with no need to recreate templates or update instance
metadata manually. The VM’s service account needs
storage.objects.get permission on the bucket to download
the file.
When to use startup scripts
Startup scripts work best for lightweight, repeatable setup tasks. Here are the most common situations where they shine:
Installing a web server on boot. A fresh VM from a public Debian or Ubuntu image has no application server installed. A startup script installs nginx and starts it automatically, no SSH session required.
Pulling config at runtime. Download an application config file from Cloud Storage or fetch a database connection string from Secret Manager. Config stays separate from the image, so you can update it without rebuilding anything.
Bootstrapping stateless VMs in a managed instance group. Every VM the group creates is ephemeral. A startup script ensures each one installs the same version of your app and starts the same services, without human involvement.
Basic fleet initialisation. Install monitoring agents, configure log forwarders, or register the VM with an internal service registry at boot.
Lab and learning environments. Pre-configure a VM for a workshop or demo so participants arrive at a ready environment. Create the VM from scratch and have the script install tools and clone repos automatically.
Startup scripts vs custom VM images
Both startup scripts and custom VM images give you VMs that start in a known state. They solve the same problem in different ways, and understanding the trade-offs helps you pick the right one — or decide to use both.
A startup script is like a chef who arrives at an empty kitchen every morning and cooks from scratch. Flexible and easy to change the menu, but there is always a delay before the first meal is ready. A custom image is like a chef who preps and vacuum-seals the meals the night before. Faster to serve, but changing the menu requires preparing a new batch.
Startup scripts are flexible. You update the script in Cloud Storage and the next boot picks it up with no build process. They work well for lightweight setup, runtime config, and logic that changes frequently. The downside: every boot runs setup from scratch, which adds time and can fail if a package repository or external API is temporarily unavailable.
Custom images bake software directly onto the disk before the VM is ever created. Boot time is fast and consistent. The trade-off is that building, testing, and maintaining images takes more effort. Every OS update or package change requires a new image build.
Many teams use both. A custom image handles the base OS, security hardening, and heavy dependencies like language runtimes or databases. A startup script handles lighter, environment-specific work: pulling a config file, fetching a secret, and starting the application.
| Startup script | Custom image | |
|---|---|---|
| Setup method | Script runs at each boot | Software baked in before creation |
| Update process | Edit the script; takes effect next boot | Rebuild and re-release the image |
| Boot speed | Slower if installs are heavy | Fast — nothing to install at runtime |
| Failure risk | Can fail if repo or API unavailable | Fails at image build time, not runtime |
| Best for | Lightweight config, evolving logic, pulling secrets | Heavy deps, regulated environments, fast autoscaling |
| Complexity | Low | Higher — requires a build and release process |
Debugging a startup script
When a startup script fails, the VM often boots normally from the OS perspective. Health checks may pass while your application is not running. Knowing where to look makes the difference between a 2-minute fix and 30 minutes of guessing.
Serial console output
The serial console captures everything the startup script prints to stdout and stderr, even before SSH is available. Check this first.
gcloud compute instances get-serial-port-output my-web-server \
--zone=us-central1-aScroll to the section starting with
Starting Google Compute Engine Startup Scripts. The last line
before output cuts off is usually where the script failed.
journalctl after SSH
Once you can SSH into the VM, view the full startup script service log:
sudo journalctl -u google-startup-scripts.serviceThis shows complete output with timestamps. Use —no-pager
to print everything at once, or pipe to grep -i error to
filter for failures.
Checking syslog
sudo grep -i startup /var/log/syslogOn Debian and Ubuntu, syslog captures startup script messages alongside other system events. Useful when you want a wider picture of what was happening on the system when the script ran.
Common reasons scripts fail
Package manager not yet ready. The script ran before the network was fully up, so
apt-get updatefailed. Add a short retry before the first package install if you see this on fresh VMs.Bad shebang line. A script starting with
#!/bin/shmay break on bash-specific syntax. Use#!/bin/bashunless you have tested for POSIX compatibility.Syntax errors. Run
bash -n script.shlocally to check for syntax problems before uploading the script.Permission denied. Mounted Cloud Storage FUSE paths or network shares may not be writable as root. Check the error message for the exact path.
Missing APIs or IAM roles. Calls to
gcloud secretsfail if the Secret Manager API is not enabled or the VM’s service account lacks the required role. The error is usually a clear 403 or “API not enabled” message in the log.
Test your script on a throwaway VM before baking it into an instance
template. Create a test VM with the script inline, SSH in, run
sudo journalctl -u google-startup-scripts.service, and
confirm the output looks right before moving to the template.
Common mistakes
Embedding credentials in the script. Scripts are stored in instance metadata and readable by anyone with
compute.instances.getpermission. Use Secret Manager instead and fetch secrets at boot time using the VM’s service account.Not using
set -e. Without it, bash continues past a failed command. The VM appears healthy while required software was never installed. Addset -eat the top so failures surface immediately in the serial console.Assuming the script runs only once. Scripts run on every boot. Actions that should happen once — creating a file, seeding a database, registering with an external service — need a guard to check whether they have already run.
Assuming the VM is application-ready as soon as it boots. A VM can pass a TCP health check while the startup script is still running. Check the serial console output before assuming the application is ready.
Not checking logs when the app does not start. The most common debugging mistake is looking everywhere except the startup script logs. Check the serial console output first — it is the only place you can see what happened before SSH was available.
Using startup scripts for heavy provisioning. Installing large runtimes or pulling gigabytes of data at boot makes startup slow and brittle. Anything that takes more than a minute or two to install belongs in a custom image instead.
Security considerations
Startup scripts are powerful, but their default storage mechanism creates a real security risk if you are not careful.
Scripts stored via the startup-script metadata key are
visible in plaintext to any user with compute.instances.get
permission, which is included in broad roles like
roles/compute.viewer. Never store passwords, API keys,
database credentials, or service account key files in a startup script.
The correct pattern is to fetch secrets at runtime from Secret Manager using the VM’s attached service account. The VM authenticates automatically. No key file is needed in the script:
#!/bin/bash
set -e
# Fetch the database password at boot using the VM's service account identity
DB_PASSWORD=$(gcloud secrets versions access latest \
--secret="my-db-password" \
--project="my-project-id")
# Use the secret to configure the application
echo "DB_PASSWORD=${DB_PASSWORD}" >> /etc/app/config.env
systemctl restart my-appAdditional security practices to follow:
Scope service account permissions narrowly. Avoid attaching the broad
cloud-platformscope unless the script genuinely needs it. Grant only the IAM roles the script actually calls. The principle of least privilege applies here as much as anywhere else.Restrict the GCS bucket holding the script. If you use
startup-script-url, the bucket should not be publicly readable. Grant the VM’s service accountstorage.objects.geton that bucket only.Grant Secret Manager access at the secret level. The VM’s service account needs
roles/secretmanager.secretAccessoron the specific secret, not on all secrets in the project.
Shutdown scripts
A shutdown script is the counterpart to a startup script. It runs when the VM is stopped or restarted, before the OS shuts down. Use it to drain active connections, flush logs to a persistent store, or deregister the VM from a service registry so traffic stops routing to it before it disappears.
A Linux shutdown script has roughly 90 seconds to complete. Keep it short and focused on cleanup, not heavy processing.
gcloud compute instances create my-web-server \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--zone=us-central1-a \
--metadata=shutdown-script='#!/bin/bash
systemctl stop nginx
echo "Shutdown complete at $(date)" >> /var/log/shutdown.log'Shutdown scripts are most useful when your application holds in-flight state that needs to be gracefully flushed. If the VM is behind a load balancer, a shutdown script can stop accepting new traffic and allow existing requests to finish before the instance disappears.
Startup scripts in instance templates and managed instance groups
When you run a fleet of VMs with a managed instance group, the startup script belongs in the instance template. Every VM the group creates — whether to meet a minimum size, respond to autoscaling, or replace an unhealthy instance — inherits the template and runs the same script automatically.
# Create a template that fetches its script from Cloud Storage
gcloud compute instance-templates create web-server-template \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--metadata=startup-script-url=gs://my-config-bucket/startup.sh \
--service-account=my-sa@my-project.iam.gserviceaccount.com \
--scopes=cloud-platformUsing startup-script-url in the template rather than
embedding the script inline has a practical advantage: you can update
the script in Cloud Storage without creating a new template version.
The next VM boot picks up the updated script automatically.
Updating the script in Cloud Storage does not affect VMs that are currently running. Existing VMs will not re-run the script until they restart or are replaced. In a managed instance group, trigger a rolling restart to propagate the change across the fleet.
Instance templates are immutable once created. If you embed the script inline and later need to change it, you must create a new template version. The GCS URL approach avoids this for script changes, though it does not help for other settings like machine type or disk size, which always require a new template.
Summary
- Startup scripts run on every boot via the
startup-scriptorstartup-script-urlmetadata key. Write them to be idempotent. - Pass scripts inline for quick tests, from a local file for source-controlled scripts, or via Cloud Storage URL for scripts shared across many VMs.
- Debug with
gcloud compute instances get-serial-port-outputbefore SSH is available, then withjournalctl -u google-startup-scripts.serviceafter. - Never embed credentials in startup scripts. Fetch them at boot from Secret Manager using the VM’s service account.
- Use
startup-script-urlin instance templates so you can update the script in Cloud Storage without creating a new template version. - For heavy dependencies that slow down boot, bake them into a custom image instead.
Frequently asked questions
When does a startup script run in Compute Engine?
A startup script runs on every boot — including the first boot after creation, manual restarts, and when a managed instance group creates a replacement VM. It runs as root via the google-startup-scripts.service systemd unit. Because it runs on every boot, write your script to be idempotent: running it twice should produce the same result as running it once. Package installs with apt are idempotent by default, but file creation, API registration, and database seeding may not be.
What is the difference between startup-script and startup-script-url?
startup-script is a metadata key that holds the script text directly. startup-script-url is a metadata key that holds a Cloud Storage path (gs://bucket/file.sh) from which the VM downloads and runs the script at boot. Use startup-script for short one-off scripts or quick testing. Use startup-script-url for scripts shared across multiple VMs or instance templates — you update the script in one place without needing to recreate or re-version every template.
How do I debug a startup script that failed?
Check the serial console output first: gcloud compute instances get-serial-port-output VM_NAME --zone=ZONE. After SSHing in, run sudo journalctl -u google-startup-scripts.service to see the full script output. Look for the last successful line before the output stops — that is usually where the failure occurred. Common causes include missing packages, network not yet ready, permission denied errors, and syntax mistakes in the shebang line.
Should I use startup scripts or custom images?
It depends on what you are doing. Startup scripts are flexible and easy to update — change the script and the next boot picks it up. Custom images are faster because the software is pre-baked onto disk. Use startup scripts when your setup is lightweight, changes often, or pulls config at runtime. Use a custom image when you have heavy dependencies that take minutes to install, or when boot time matters for autoscaling. Many teams combine both: a custom image handles the base OS and core packages, while a startup script handles runtime config and secrets.
Are startup scripts safe for storing secrets?
No. Startup scripts are stored in VM instance metadata, which is visible to any user with compute.instances.get permission on the project. Never put passwords, API keys, or tokens directly in a startup script. Instead, fetch them at boot time from Secret Manager using the VM's attached service account. The VM authenticates automatically — no key file is needed in the script.