GCP for DevOps — Startup Scripts
Startup Scripts
A startup script runs automatically when a VM instance starts (boot or restart). It’s the simplest way to automate VM provisioning — installing packages, configuring services, and pulling application code.
How Startup Scripts Work
VM Boot Sequence:
│
├── 1. BIOS / Firmware
├── 2. Boot loader (GRUB)
├── 3. Kernel loads
├── 4. systemd / init starts
├── 5. google-startup-scripts.service runs ◄── Your script
└── 6. VM is ready
Startup scripts run as root and execute every time the VM starts (not just first boot, unless you add a guard).
Inline Startup Script
# Pass a startup script directly on the command line
$ gcloud compute instances create web-server \
--zone=us-central1-a \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--tags=http-server \
--metadata=startup-script='#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>Hello from $(hostname)</h1>" > /var/www/html/index.html
'
Startup Script from a File
# Create a startup script file
$ cat > startup.sh << 'EOF'
#!/bin/bash
set -e
# Update packages
apt-get update
apt-get install -y docker.io docker-compose
# Start Docker
systemctl enable docker
systemctl start docker
# Pull and run the application
docker pull us-docker.pkg.dev/my-project/my-repo/app:latest
docker run -d -p 80:8080 --name app \
us-docker.pkg.dev/my-project/my-repo/app:latest
# Signal that startup is complete
curl -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/attributes/startup-status" \
|| true
echo "Startup complete at $(date)" >> /var/log/startup.log
EOF
# Create the VM with the script file
$ gcloud compute instances create app-server \
--zone=us-central1-a \
--machine-type=e2-standard-2 \
--image-family=debian-12 \
--image-project=debian-cloud \
--metadata-from-file=startup-script=startup.sh
Startup Script from Cloud Storage
For large or frequently updated scripts, store them in GCS.
# Upload the script to a bucket
$ gcloud storage cp startup.sh gs://my-scripts-bucket/startup.sh
# Create a VM that pulls the script from GCS
$ gcloud compute instances create app-server \
--zone=us-central1-a \
--machine-type=e2-standard-2 \
--image-family=debian-12 \
--image-project=debian-cloud \
--metadata=startup-script-url=gs://my-scripts-bucket/startup.sh \
--scopes=storage-ro
Tip: The VM’s service account needs
roles/storage.objectVieweron the bucket to download the script.
Reading Metadata in Startup Scripts
Access instance metadata from within the VM to make scripts dynamic.
#!/bin/bash
# Read metadata values
PROJECT=$(curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/project/project-id)
ZONE=$(curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/zone | cut -d'/' -f4)
INSTANCE_NAME=$(curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/name)
# Read custom metadata
APP_VERSION=$(curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/attributes/app-version)
echo "Deploying $APP_VERSION to $INSTANCE_NAME in $ZONE (project: $PROJECT)"
Shutdown Scripts
Run cleanup tasks before a VM stops or is deleted.
$ gcloud compute instances create app-server \
--zone=us-central1-a \
--machine-type=e2-medium \
--image-family=debian-12 \
--image-project=debian-cloud \
--metadata=startup-script='#!/bin/bash
echo "Starting app" >> /var/log/lifecycle.log
',shutdown-script='#!/bin/bash
echo "Draining connections..." >> /var/log/lifecycle.log
# Deregister from load balancer
# Flush buffers
# Upload logs to GCS
gcloud storage cp /var/log/app.log gs://my-logs/$(hostname)/
echo "Shutdown complete" >> /var/log/lifecycle.log
'
Debugging Startup Scripts
# View startup script output (serial port)
$ gcloud compute instances get-serial-port-output app-server \
--zone=us-central1-a | tail -50
# SSH and check the log
$ gcloud compute ssh app-server --zone=us-central1-a
$ sudo journalctl -u google-startup-scripts.service
# Check if the script ran
$ sudo cat /var/log/syslog | grep startup-script
Mar 30 12:00:05 app-server startup-script: INFO Starting startup scripts.
Mar 30 12:00:06 app-server startup-script: INFO startup-script: Deploying...
Mar 30 12:00:30 app-server startup-script: INFO startup-script: Startup complete
Mar 30 12:00:30 app-server startup-script: INFO Finished running startup scripts.
Run-Once Pattern
To run a script only on first boot (not on restart):
#!/bin/bash
MARKER="/var/run/startup-complete"
if [ -f "$MARKER" ]; then
echo "Startup script already ran. Skipping."
exit 0
fi
# Your one-time setup
apt-get update
apt-get install -y docker.io
systemctl enable docker
systemctl start docker
# Mark as complete
touch "$MARKER"
echo "First-boot setup complete"
Advanced: Cloud-Init
For more complex provisioning, use cloud-init (supported on most GCP images).
$ gcloud compute instances create cloud-init-vm \
--zone=us-central1-a \
--machine-type=e2-medium \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--metadata-from-file=user-data=cloud-init.yaml
# cloud-init.yaml
#cloud-config
package_update: true
packages:
- docker.io
- htop
- jq
users:
- name: deployer
groups: docker
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
write_files:
- path: /etc/docker/daemon.json
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
runcmd:
- systemctl enable docker
- systemctl start docker
Common Mistakes
| Mistake | Why It’s Wrong | Fix |
|---|---|---|
Not using set -e |
Script continues after errors | Add set -e at the top |
| Not checking serial output | Hard to debug failures | Always check serial port output |
| Script runs on every boot | Reinstalls packages every restart | Use the run-once pattern |
| Hardcoding project/zone | Script not portable | Read from metadata server |
| Large inline scripts | Hard to maintain and version | Store in GCS and version control |
Quick Reference
| Command | Description |
|---|---|
--metadata=startup-script='...' |
Inline startup script |
--metadata-from-file=startup-script=FILE |
Script from local file |
--metadata=startup-script-url=gs://BUCKET/SCRIPT |
Script from GCS |
--metadata=shutdown-script='...' |
Inline shutdown script |
gcloud compute instances get-serial-port-output VM |
View script output |
sudo journalctl -u google-startup-scripts.service |
View script logs on VM |
Next → Managed Instance Groups