Startup scripts automate VM configuration by running commands automatically when a Compute Engine instance boots. This covers how they work, how to specify them, and practical examples for real workloads.
What Are Startup Scripts?
A startup script is a file that runs commands when a VM instance boots. Compute Engine copies the script to the VM, sets executable permissions, and runs it as the root user (on Linux) during boot.
Key facts:
| Aspect | Behavior |
|---|---|
| When it runs | On every boot by default (not just the first boot) |
| What user | root on Linux, System on Windows |
| Size limit | 256 KB for inline/local scripts. Use Cloud Storage for larger scripts. |
| Scope | VM-level or project-level. VM-level overrides project-level. |
| Prerequisite | Guest environment (google-guest-agent) must be installed. All public images include it. |
| Network | Startup scripts only run when a network is available. |
Key Insight: Unlike AWS EC2
user-data(which runs only on first launch by default), GCE startup scripts run on every boot. If you need first-boot-only behavior, you must handle it yourself (see Running Only on First Boot).
Metadata Keys for Linux
Linux startup scripts use these metadata keys:
| Metadata Key | Use For | Size Limit |
|---|---|---|
startup-script | Script passed directly or stored locally. Bash or non-bash (with shebang). | Up to 256 KB |
startup-script-url | Script stored in a Cloud Storage bucket. | Greater than 256 KB |
Execution Order (Linux)
When multiple startup scripts are configured:
| Metadata Key | Order |
|---|---|
startup-script | Runs first, on each boot after the initial boot |
startup-script-url | Runs second, on each boot after the initial boot |
How to Specify Startup Scripts
1. Inline via Metadata (gcloud CLI)
gcloud compute instances create my-vm \
--zone=us-central1-a \
--image-family=debian-12 \
--image-project=debian-cloud \
--metadata=startup-script='#!/bin/bash
apt-get update
apt-get install -y nginx
echo "Hello from startup script" > /var/www/html/index.html'Add to an existing VM:
gcloud compute instances add-metadata my-vm \
--zone=us-central1-a \
--metadata=startup-script='#!/bin/bash
echo "Updated startup script"'2. From a Local File
Store the script on your workstation and pass it at creation time:
gcloud compute instances create my-vm \
--zone=us-central1-a \
--image-family=debian-12 \
--image-project=debian-cloud \
--metadata-from-file=startup-script=./startup.shThe startup.sh file:
#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx3. From Cloud Storage
For scripts larger than 256 KB, store them in a GCS bucket:
# First, upload your script to GCS
gcloud storage cp startup.sh gs://my-bucket/scripts/startup.sh
# Then reference it when creating the VM
gcloud compute instances create my-vm \
--zone=us-central1-a \
--image-family=debian-12 \
--image-project=debian-cloud \
--scopes=storage-ro \
--metadata=startup-script-url=gs://my-bucket/scripts/startup.shThe VM’s service account must have permission to read the object, such as roles/storage.objectViewer on the bucket or object. The --scopes=storage-ro flag allows Cloud Storage read access at the OAuth scope layer, but IAM still controls whether the service account can download the script.
You can also use the authenticated URL format:
https://storage.googleapis.com/BUCKET/FILE
Warning: If the GCS bucket is less secure than VM metadata, there’s a privilege escalation risk. Anyone who can modify the script and trigger a reboot gets root-level execution on the VM. Lock down your bucket permissions.
4. Via Google Cloud Console
- Go to Compute Engine → VM instances → Create instance
- Expand Advanced options
- Under Management → Automation, enter your startup script in the text box
- Or under Metadata, add a key
startup-script-urlwith the GCS URL as value
Special Behaviors
Running Only on First Boot
GCE does not have a built-in “run once” setting for Linux. Startup scripts run on every boot. The standard workaround is a sentinel file check:
#!/bin/bash
# Only run on the first boot
if [ -f /etc/startup_completed ]; then
exit 0
fi
# Your one-time setup commands here
apt-get update
apt-get install -y nginx
# Mark as done
touch /etc/startup_completedAn alternative is using guest attributes to store a flag in the metadata server, but the file approach is simpler and more secure (only root can modify a root-owned file).
Rerunning a Startup Script Manually
You can rerun the startup script without rebooting:
sudo google_metadata_script_runner startupChecking if a Startup Script Succeeded
| Method | Command / Steps |
|---|---|
| journalctl (Linux) | sudo journalctl -u google-startup-scripts.service |
| Serial console | View serial port 1 output in Console. Look for google_metadata_script_runner events. |
| syslog | grep "startup-script exit status" /var/log/syslog — status 0 is success, 1 is failure. |
| Cloud Logging | Filter: resource.type=gce_instance AND jsonPayload.message=~"^startup-script:" |
| Guest attributes | The guest agent writes script status to guest attributes. Query via metadata server. |
# Check guest attribute for startup script status
curl http://metadata.google.internal/computeMetadata/v1/instance/guest-attributes/startup-script/status \
-H "Metadata-Flavor: Google"What Happens if a Startup Script Fails
If a startup script exits with a non-zero code, the VM still starts and becomes running. The failure is logged but does not prevent the VM from booting. The google-startup-scripts.service systemd unit will show a failed status.
# Check service status
sudo systemctl status google-startup-scripts.serviceFor managed instance groups (MIGs), you can configure health checks and autohealing to detect and replace VMs where startup scripts failed.
Accessing Metadata from a Startup Script
You can read custom metadata values inside a startup script, which lets you use the same script across multiple VMs with different parameters:
#!/bin/bash
# Read a custom metadata value
ROLE=$(curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/role \
-H "Metadata-Flavor: Google")
echo "This VM's role is: $ROLE"Pass the metadata value when creating the VM:
gcloud compute instances create my-vm \
--metadata=startup-script='#!/bin/bash
ROLE=$(curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/role -H "Metadata-Flavor: Google")
echo "Role: $ROLE" > /tmp/role.txt' \
--metadata=role=webserverTip: Dollar signs in inline startup scripts get evaluated at injection time. If you want a
$to survive into the script itself, escape it as\$.
Practical Examples
1. Install and Configure Nginx Web Server
#!/bin/bash
apt-get update
apt-get install -y nginx
cat > /var/www/html/index.html <<'HTML'
<!DOCTYPE html>
<html>
<head><title>GCE Web Server</title></head>
<body><h1>Hello from GCE!</h1></body>
</html>
HTML
systemctl enable nginx
systemctl start nginxUse with gcloud:
gcloud compute instances create web-server \
--zone=us-central1-a \
--machine-type=e2-micro \
--image-family=debian-12 \
--image-project=debian-cloud \
--tags=http-server \
--metadata-from-file=startup-script=./nginx-setup.shThe
--tags=http-servertag must match a firewall rule allowing TCP:80 for the VM to serve traffic.
2. Install Docker and Run a Container
#!/bin/bash
# Install Docker
apt-get update
apt-get install -y ca-certificates curl gnupg
# Add Docker's official GPG key and repository
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Run an nginx container
docker run -d -p 80:80 --restart=always --name web nginx:latest3. Update Packages and Install Basic Tools
#!/bin/bash
apt-get update
apt-get upgrade -y
apt-get install -y \
curl \
wget \
git \
vim \
htop \
tree \
jq \
net-tools \
unzip \
ca-certificatesTip:
apt-get upgrade -ycan take a while and may prompt for service restarts. For production VMs, prefer building a custom image with tools pre-installed rather than installing on every boot.
4. Configure PostgreSQL Database
#!/bin/bash
# Install PostgreSQL
apt-get update
apt-get install -y postgresql postgresql-contrib
# Start the service
systemctl enable postgresql
systemctl start postgresql
# Create a database and user
sudo -u postgres psql -c "CREATE USER appuser WITH PASSWORD 'changeme';"
sudo -u postgres psql -c "CREATE DATABASE appdb OWNER appuser;"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser;"
# Allow connections from authorized hosts
echo "host appdb appuser 10.0.0.0/8 md5" >> /etc/postgresql/15/main/pg_hba.conf
systemctl restart postgresqlWarning: Never hardcode real passwords in startup scripts for production. Use Secret Manager and pull secrets at boot time instead.
5. Download and Run an App from Cloud Storage
#!/bin/bash
# This VM needs Storage IAM permissions and a scope that allows Storage access
BUCKET="my-app-bucket"
APP_DIR="/opt/myapp"
mkdir -p $APP_DIR
# Download the application artifact
gcloud storage cp gs://$BUCKET/releases/app-v1.0.tar.gz /tmp/app.tar.gz
tar -xzf /tmp/app.tar.gz -C $APP_DIR
# Install dependencies and run
cd $APP_DIR
chmod +x start.sh
./start.sh &6. Set Up Monitoring and Register with a System
#!/bin/bash
# Install the Ops Agent for Cloud Monitoring and Cloud Logging
curl -sSO https://dl.google.com/cloudagents/add-google-cloud-ops-agent-repo.sh
bash add-google-cloud-ops-agent-repo.sh --also-install
# Get this VM's metadata for registration
INSTANCE_NAME=$(curl http://metadata.google.internal/computeMetadata/v1/instance/name -H "Metadata-Flavor: Google")
INSTANCE_ZONE=$(curl http://metadata.google.internal/computeMetadata/v1/instance/zone -H "Metadata-Flavor: Google" | awk -F/ '{print $NF}')
INSTANCE_ID=$(curl http://metadata.google.internal/computeMetadata/v1/instance/id -H "Metadata-Flavor: Google")
PROJECT_ID=$(curl http://metadata.google.internal/computeMetadata/v1/project/project-id -H "Metadata-Flavor: Google")
# Register with an external monitoring or service discovery system
curl -X POST https://monitoring.example.com/register \
-H "Content-Type: application/json" \
-d "{\"instance\":\"$INSTANCE_NAME\",\"zone\":\"$INSTANCE_ZONE\",\"project\":\"$PROJECT_ID\"}"7. Set Up a Cron Job
#!/bin/bash
# Create a backup script
cat > /opt/backup.sh <<'SCRIPT'
#!/bin/bash
DATE=$(date +%Y-%m-%d)
gcloud storage cp -r /var/lib/appdata gs://backup-bucket/daily/$DATE/
SCRIPT
chmod +x /opt/backup.sh
# Schedule it to run daily at 2 AM
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/backup.sh >> /var/log/backup.log 2>&1") | crontab -8. First-Boot Setup with Sentinel File
Combining the sentinel pattern with a real setup. This runs the full installation only once:
#!/bin/bash
SENTINEL="/etc/gce_first_boot_done"
if [ -f "$SENTINEL" ]; then
echo "First boot already completed. Skipping."
exit 0
fi
# One-time setup
apt-get update
apt-get install -y nginx docker.io
# Configure the application
cat > /var/www/html/index.html <<'HTML'
<h1>Configured on first boot</h1>
HTML
systemctl enable nginx
systemctl start nginx
# Signal completion
touch "$SENTINEL"
echo "First boot setup complete at $(date)" >> /var/log/first-boot.logWindows Startup Scripts
Windows VMs use different metadata keys. The key difference is that Windows supports two phases of execution:
| Phase | When | Keys |
|---|---|---|
| Specialize (sysprep) | Only during the initial boot | sysprep-specialize-script-ps1, sysprep-specialize-script-cmd, sysprep-specialize-script-bat, sysprep-specialize-script-url |
| Every boot | On each boot after the initial boot | windows-startup-script-ps1, windows-startup-script-cmd, windows-startup-script-bat, windows-startup-script-url |
Windows Metadata Keys
| Metadata Key | Script Type | Size Limit |
|---|---|---|
sysprep-specialize-script-ps1 | PowerShell (initial boot only) | 256 KB |
sysprep-specialize-script-cmd | Command shell (initial boot only) | 256 KB |
sysprep-specialize-script-bat | Batch file (initial boot only) | 256 KB |
sysprep-specialize-script-url | From Cloud Storage (initial boot only) | >256 KB |
windows-startup-script-ps1 | PowerShell (every boot) | 256 KB |
windows-startup-script-cmd | Command shell (every boot) | 256 KB |
windows-startup-script-bat | Batch file (every boot) | 256 KB |
windows-startup-script-url | From Cloud Storage (every boot) | >256 KB |
Windows Execution Order
During initial boot (specialize phase):
| Order | Metadata Key |
|---|---|
| 1st | sysprep-specialize-script-ps1 |
| 2nd | sysprep-specialize-script-cmd |
| 3rd | sysprep-specialize-script-bat |
| 4th | sysprep-specialize-script-url |
During each subsequent boot:
| Order | Metadata Key |
|---|---|
| 1st | windows-startup-script-ps1 |
| 2nd | windows-startup-script-cmd |
| 3rd | windows-startup-script-bat |
| 4th | windows-startup-script-url |
Example: Windows PowerShell Startup Script
gcloud compute instances create my-windows-vm \
--image-family=windows-2022 \
--image-project=windows-cloud \
--metadata=windows-startup-script-ps1='Install-WindowsFeature -Name Web-Server
Set-Content -Path "C:\inetpub\wwwroot\index.html" -Value "<h1>Hello from GCE Windows</h1>"'Key Insight: Windows has a built-in “run once” mechanism via
sysprep-specialize-script-*keys that only run during the initial boot. Linux does not have this — you need the sentinel file pattern described above.
Best Practices
| Practice | Why |
|---|---|
| Use idempotent scripts | Scripts run on every boot. Writing scripts that produce the same result whether run once or ten times avoids state drift. |
Use startup-script-url for large scripts | Inline scripts are limited to 256 KB. Store larger scripts in GCS. |
| Use custom images for complex setups | If your startup script installs many packages, bake them into a custom image instead. Faster boots, more reliable. |
| Log output for debugging | Add set -x at the top of bash scripts to trace execution. Check journalctl -u google-startup-scripts.service for output. |
| Handle failures explicitly | Use set -e to stop on errors, or add your own error handling with ` |
| Don’t hardcode secrets | Use Secret Manager and grant the VM service account only the secrets it needs. Metadata values are visible in the Console and API. |
| Escape dollar signs in inline scripts | $HOME and $USER get evaluated at injection time, not at runtime. Use \$HOME if you want the literal string. |
| Use sentinel files for first-boot logic | See the Running Only on First Boot section. |
| Set reasonable expectations | Startup scripts run after networking is up but before the VM is fully “ready.” Expect 30-60 seconds for simple scripts, longer for package installs. |
Common Pitfalls
| Pitfall | What Goes Wrong | Fix |
|---|---|---|
| Script runs on every boot | Package installs, user creation repeated on every restart | Use a sentinel file to guard one-time operations |
| Dollar sign evaluation | Variables like $HOME become empty strings in inline scripts | Escape as \$HOME or use metadata-from-file |
| No firewall rule for web traffic | Nginx/Apache is running but unreachable from the internet | Add --tags=http-server and a firewall rule for TCP:80 |
| Missing Storage access | startup-script-url can’t download from GCS | Grant the VM service account Storage Object Viewer and use a scope that allows Storage read access |
| Custom image without guest agent | Startup scripts silently don’t run | Install google-guest-agent on custom images |
| Large inline script | Metadata value exceeds 256 KB limit | Move to Cloud Storage and use startup-script-url |
TL;DR
- Startup scripts run as root on every boot (not just the first one).
- Use
startup-scriptfor inline/local scripts (up to 256 KB),startup-script-urlfor Cloud Storage scripts. - Specify via Console, gcloud (
--metadataor--metadata-from-file), or REST API. - For first-boot-only behavior on Linux, use a sentinel file check at the top of your script.
- Windows has separate metadata keys:
sysprep-specialize-script-ps1for initial boot,windows-startup-script-ps1for every boot. - Check results with
sudo journalctl -u google-startup-scripts.service. - For complex setups, prefer building a custom image over a long startup script.
Resources
About startup scripts Official overview of startup scripts on Compute Engine.
Use startup scripts on Linux VMs Full guide for Linux startup scripts with Console, gcloud, and REST examples.
Use startup scripts on Windows VMs Full guide for Windows startup scripts including PowerShell and batch.
Predefined metadata keys Complete reference for all metadata keys supported by Compute Engine.
Creating Your First VM Step-by-step guide that uses startup scripts in the quickstart examples.
Google Compute Engine Overview of GCE features and architecture.