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:

AspectBehavior
When it runsOn every boot by default (not just the first boot)
What userroot on Linux, System on Windows
Size limit256 KB for inline/local scripts. Use Cloud Storage for larger scripts.
ScopeVM-level or project-level. VM-level overrides project-level.
PrerequisiteGuest environment (google-guest-agent) must be installed. All public images include it.
NetworkStartup 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 KeyUse ForSize Limit
startup-scriptScript passed directly or stored locally. Bash or non-bash (with shebang).Up to 256 KB
startup-script-urlScript stored in a Cloud Storage bucket.Greater than 256 KB

Execution Order (Linux)

When multiple startup scripts are configured:

Metadata KeyOrder
startup-scriptRuns first, on each boot after the initial boot
startup-script-urlRuns 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.sh

The startup.sh file:

#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx

3. 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.sh

The 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

  1. Go to Compute Engine VM instances Create instance
  2. Expand Advanced options
  3. Under Management Automation, enter your startup script in the text box
  4. Or under Metadata, add a key startup-script-url with 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_completed

An 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 startup

Checking if a Startup Script Succeeded

MethodCommand / Steps
journalctl (Linux)sudo journalctl -u google-startup-scripts.service
Serial consoleView serial port 1 output in Console. Look for google_metadata_script_runner events.
sysloggrep "startup-script exit status" /var/log/syslog — status 0 is success, 1 is failure.
Cloud LoggingFilter: resource.type=gce_instance AND jsonPayload.message=~"^startup-script:"
Guest attributesThe 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.service

For 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=webserver

Tip: 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 nginx

Use 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.sh

The --tags=http-server tag 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:latest

3. 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-certificates

Tip: apt-get upgrade -y can 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 postgresql

Warning: 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.log

Windows Startup Scripts

Windows VMs use different metadata keys. The key difference is that Windows supports two phases of execution:

PhaseWhenKeys
Specialize (sysprep)Only during the initial bootsysprep-specialize-script-ps1, sysprep-specialize-script-cmd, sysprep-specialize-script-bat, sysprep-specialize-script-url
Every bootOn each boot after the initial bootwindows-startup-script-ps1, windows-startup-script-cmd, windows-startup-script-bat, windows-startup-script-url

Windows Metadata Keys

Metadata KeyScript TypeSize Limit
sysprep-specialize-script-ps1PowerShell (initial boot only)256 KB
sysprep-specialize-script-cmdCommand shell (initial boot only)256 KB
sysprep-specialize-script-batBatch file (initial boot only)256 KB
sysprep-specialize-script-urlFrom Cloud Storage (initial boot only)>256 KB
windows-startup-script-ps1PowerShell (every boot)256 KB
windows-startup-script-cmdCommand shell (every boot)256 KB
windows-startup-script-batBatch file (every boot)256 KB
windows-startup-script-urlFrom Cloud Storage (every boot)>256 KB

Windows Execution Order

During initial boot (specialize phase):

OrderMetadata Key
1stsysprep-specialize-script-ps1
2ndsysprep-specialize-script-cmd
3rdsysprep-specialize-script-bat
4thsysprep-specialize-script-url

During each subsequent boot:

OrderMetadata Key
1stwindows-startup-script-ps1
2ndwindows-startup-script-cmd
3rdwindows-startup-script-bat
4thwindows-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

PracticeWhy
Use idempotent scriptsScripts 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 scriptsInline scripts are limited to 256 KB. Store larger scripts in GCS.
Use custom images for complex setupsIf your startup script installs many packages, bake them into a custom image instead. Faster boots, more reliable.
Log output for debuggingAdd set -x at the top of bash scripts to trace execution. Check journalctl -u google-startup-scripts.service for output.
Handle failures explicitlyUse set -e to stop on errors, or add your own error handling with `
Don’t hardcode secretsUse 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 logicSee the Running Only on First Boot section.
Set reasonable expectationsStartup 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

PitfallWhat Goes WrongFix
Script runs on every bootPackage installs, user creation repeated on every restartUse a sentinel file to guard one-time operations
Dollar sign evaluationVariables like $HOME become empty strings in inline scriptsEscape as \$HOME or use metadata-from-file
No firewall rule for web trafficNginx/Apache is running but unreachable from the internetAdd --tags=http-server and a firewall rule for TCP:80
Missing Storage accessstartup-script-url can’t download from GCSGrant the VM service account Storage Object Viewer and use a scope that allows Storage read access
Custom image without guest agentStartup scripts silently don’t runInstall google-guest-agent on custom images
Large inline scriptMetadata value exceeds 256 KB limitMove 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-script for inline/local scripts (up to 256 KB), startup-script-url for Cloud Storage scripts.
  • Specify via Console, gcloud (--metadata or --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-ps1 for initial boot, windows-startup-script-ps1 for 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.