GCP for DevOps — Startup Scripts

3 minute read

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.objectViewer on 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

Updated: