Google Compute Engine is, in my experience, the fastest of the big three clouds to get a working Docker host on — largely because Google ships an official Container-Optimized OS image that has Docker preinstalled and hardened out of the box. This guide covers both that fast path and the more customizable route of installing Docker on a plain Ubuntu image, plus how to secure and monitor whichever one you choose.
Prerequisites
- A Google Cloud project with billing enabled
gcloudCLI installed locally, or run via container (see the companion guide on cloud CLIs in Docker)- An SSH key pair (gcloud manages this for you automatically on first connect if you don’t have one)
Step 1: Authenticate and Set Your Project
gcloud auth login
gcloud config set project my-docker-project
gcloud config set compute/zone us-central1-a
Step 2: Enable the Compute Engine API
gcloud services enable compute.googleapis.com
Option A: Fast Path with Container-Optimized OS
Container-Optimized OS (COS) is Google’s minimal, locked-down Linux distribution built specifically to run containers securely, with Docker (and containerd) preinstalled.
gcloud compute instances create docker-host-01 \
--zone=us-central1-a \
--machine-type=e2-medium \
--image-family=cos-stable \
--image-project=cos-cloud \
--tags=docker-host
Expected output:
NAME ZONE MACHINE_TYPE STATUS
docker-host-01 us-central1-a e2-medium RUNNING
SSH in and Docker is already there:
gcloud compute ssh docker-host-01 --zone=us-central1-a
docker --version
docker run hello-world
Expected output:
Docker version 26.1.4, build 5650f9b
Hello from Docker!
This message shows that your installation appears to be working correctly.
Note that COS’s root filesystem is read-only by design — you can’t apt-get install anything on it. If you need extra tooling, run it as a container instead, which is the intended workflow on COS anyway.
Option B: Custom Setup on Ubuntu with Startup Script
If you want full control over the OS (custom packages, specific kernel modules, etc.), use a plain Ubuntu image with a startup script.
cat > startup-docker.sh << 'EOF'
#!/bin/bash
apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
usermod -aG docker $(logname)
systemctl enable docker
systemctl start docker
EOF
gcloud compute instances create docker-host-02 \
--zone=us-central1-a \
--machine-type=e2-medium \
--image-family=ubuntu-2404-lts-amd64 \
--image-project=ubuntu-os-cloud \
--tags=docker-host \
--metadata-from-file=startup-script=startup-docker.sh
Give it a minute for the startup script to finish, then verify:
gcloud compute ssh docker-host-02 --zone=us-central1-a --command="docker --version"
Expected output:
Docker version 27.3.1, build ce12230
Step 3: Configure Firewall Rules
GCE blocks all inbound traffic by default except what you explicitly allow via firewall rules tied to network tags (the --tags=docker-host flag above is what lets us target these instances specifically).
gcloud compute firewall-rules create allow-docker-web \
--allow=tcp:80,tcp:443 \
--target-tags=docker-host \
--source-ranges=0.0.0.0/0
gcloud compute firewall-rules create allow-docker-api \
--allow=tcp:2376 \
--target-tags=docker-host \
--source-ranges=YOUR_IP/32
Always scope the Docker API rule (2376) to a specific source IP range — never 0.0.0.0/0 — since an exposed, unauthenticated Docker daemon is equivalent to granting root access on the host.
Step 4: Securing Remote Docker API Access with TLS
Exactly as with any other cloud, exposing the Docker daemon over TCP requires TLS. On the instance:
mkdir -p ~/docker-certs && cd ~/docker-certs
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$(hostname)" -new -key server-key.pem -out server.csr
echo subjectAltName = IP:$(curl -s ifconfig.me),IP:127.0.0.1 > extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -extfile extfile.cnf
Configure /etc/docker/daemon.json:
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/ca.pem",
"tlscert": "/etc/docker/server-cert.pem",
"tlskey": "/etc/docker/server-key.pem"
}
sudo systemctl restart docker
From your laptop:
export DOCKER_HOST=tcp://EXTERNAL_IP:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/docker-certs/client
docker ps
Step 5: Attaching Persistent Disk Storage
For workloads that need storage beyond the boot disk:
gcloud compute disks create docker-data-disk \
--size=100GB \
--zone=us-central1-a \
--type=pd-ssd
gcloud compute instances attach-disk docker-host-02 \
--disk=docker-data-disk \
--zone=us-central1-a
On the instance:
sudo lsblk
sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb
sudo mkdir -p /mnt/docker-data
sudo mount /dev/sdb /mnt/docker-data
Point Docker’s data root there by adding "data-root": "/mnt/docker-data/docker" in daemon.json, then restart Docker.
Networking Fundamentals
GCE VMs sit inside a VPC network, and firewall rules there operate similarly to AWS security groups — allow-list based, applied per network tag or service account. Docker’s own internal networking (the docker0 bridge and any user-defined bridge networks you create) is entirely separate from and sits “inside” the GCE VM’s single network interface; GCE firewall rules only ever see the host’s external and internal IPs, never individual container IPs, since NAT/port publishing (-p) is what maps a host port to a container port.
Running a Real Workload
docker run -d --name web-demo -p 80:80 --restart unless-stopped nginx:latest
gcloud compute instances describe docker-host-02 \
--zone=us-central1-a \
--format='get(networkInterfaces[0].accessConfigs[0].natIP)'
Visit the returned IP in a browser to confirm Nginx is reachable.
Monitoring
docker stats --no-stream
For host-level and container-level metrics beyond ad-hoc checks, install the Google Cloud Ops Agent, which natively collects Docker container metrics and forwards them to Cloud Monitoring:
curl -sSO https://dl.google.com/cloudagents/add-google-cloud-ops-agent-repo.sh
sudo bash add-google-cloud-ops-agent-repo.sh --also-install
Deploying a Multi-Container Stack with Compose
On the Ubuntu-based host (Option B), Docker Compose is the fastest way to bring up a multi-service application:
# docker-compose.yml
services:
web:
image: nginx:latest
ports:
- "80:80"
restart: unless-stopped
api:
image: myregistry.example.com/api:1.0.0
environment:
- DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb
restart: unless-stopped
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD=apppass
restart: unless-stopped
volumes:
db-data:
docker compose up -d
On Container-Optimized OS, docker-compose isn’t preinstalled the traditional way, but you can run the Compose CLI itself as a container, or simply install the Compose plugin binary directly since COS does allow writing to /home and mounting extra tooling there.
High Availability with Managed Instance Groups
A single Compute Engine VM is a single point of failure. For production traffic, wrap your Docker host configuration in an instance template and a Managed Instance Group (MIG), fronted by a GCP HTTP(S) Load Balancer:
gcloud compute instance-templates create docker-host-template \
--machine-type=e2-medium \
--image-family=cos-stable \
--image-project=cos-cloud \
--tags=docker-host
gcloud compute instance-groups managed create docker-mig \
--base-instance-name=docker-host \
--template=docker-host-template \
--size=2 \
--zone=us-central1-a
GCP will automatically replace unhealthy instances in the group based on a configurable health check, giving you self-healing infrastructure without any orchestration layer beyond Compute Engine itself.
Understanding GCP’s Networking Layers
As with any cloud, it pays to keep the layers distinct:
- VPC network and subnet — the private IP space the instance lives in.
- Firewall rules, scoped by network tags — the layer
gcloud compute firewall-rules createoperates on; this is GCP’s equivalent of an AWS security group or Azure NSG. - Docker’s internal bridge network — invisible to GCP, handles container-to-container communication on the instance itself.
- Published ports (
-p) — the bridge between the instance’s external IP and the container’s internal port.
A container can be perfectly healthy and still be completely unreachable from the internet if any one of these layers isn’t configured — this is by far the most common “why can’t I reach my container” issue on a first GCE deployment.
Best Practices
- Prefer Container-Optimized OS for pure Docker-hosting use cases — its read-only root filesystem and automatic updates meaningfully reduce attack surface compared to a general-purpose distro.
- Use network tags and scoped firewall rules, never a blanket
0.0.0.0/0rule for anything beyond port 80/443. - Use a service account with minimal IAM roles attached to the instance rather than embedding long-lived keys.
- Snapshot persistent disks regularly (
gcloud compute disks snapshot) if container data matters. - Consider GKE Autopilot instead of a raw Compute Engine VM if you’re going to run more than a handful of services — it removes daemon and node management entirely.
Troubleshooting
gcloud compute sshhangs — firewall rule for SSH (port 22) is enabled by default via thedefault-allow-sshrule; check it hasn’t been deleted.- Startup script didn’t run — check
/var/log/syslogon Ubuntu or usegcloud compute instances get-serial-port-output docker-host-02to see boot logs, including startup-script output. - No external IP / can’t reach the instance — confirm you didn’t pass
--no-address; by defaultgcloud compute instances createassigns an ephemeral external IP unless told otherwise. - COS instance rejects
apt-get— expected behavior; COS’s root filesystem is intentionally read-only. Install tools as containers instead.
Summary
Google Compute Engine offers a genuinely fast path to a Docker host via Container-Optimized OS, where Docker is already installed, hardened, and auto-updating. For workloads needing a custom OS setup, a plain Ubuntu image with a startup script gets you there in a similar number of steps. Either way, GCE firewall rules and network tags are the layer that decides what’s reachable from outside — Docker’s own container networking remains unchanged from any other Linux host once you’re inside the VM.
References
- Container-Optimized OS documentation: https://cloud.google.com/container-optimized-os/docs
- Compute Engine documentation: https://cloud.google.com/compute/docs
- Docker Engine security documentation: https://docs.docker.com/engine/security/protect-access/
- Google Cloud Ops Agent documentation: https://cloud.google.com/monitoring/agent/ops-agent
