About Me
Projects
Notes
Online Content
Resume
© 2026 /Hadi Rouhani
BlogMarch 1, 2026

End-to-End Workflow: From Development to Production on Google Cloud

Avatar
Hadi Rouhani
End-to-End Workflow: From Development to Production on Google Cloud

Overview

This guide walks through the complete development-to-production workflow: building your application locally, containerizing it with Docker, pushing to Google Container Registry (GCR), and deploying to Cloud Run. You'll learn practical CLI commands and understand why certain architectural decisions (like multi-architecture builds on macOS) matter for production deployments. Prerequisites:
  • Local development environment set up
  • Docker installed on macOS
  • Google Cloud SDK (gcloud CLI) installed and configured
  • GCP project with billing enabled and Container Registry enabled
  • Service account with appropriate permissions

Local Development

Set up your environment locally before containerization:
Bash
# Clone or navigate to your project
cd /path/to/your/project

# For Python projects: Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# For Node.js projects: Install dependencies
npm install

# Run your application locally
python app.py    # or npm start, depending on your stack
Test your application thoroughly and verify environment variables work correctly:
Bash
# Test with environment variables
export DATABASE_URL="postgresql://user:password@localhost/db"
export API_KEY="test-key"
python app.py

Containerization

Creating a Production-Ready Dockerfile Design your Dockerfile for multi-architecture builds. This is crucial when developing on macOS (ARM64/Apple Silicon) but deploying to cloud infrastructure that may run on different architectures:
Dockerfile
# Multi-stage build optimizes image size
# Stage 1: Builder
FROM python:3.11-slim as builder

WORKDIR /build

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.11-slim

WORKDIR /app

# Copy only necessary artifacts from builder
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

# Copy application code
COPY . .

# Non-root user for security
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

# Expose port (Cloud Run defaults to 8080)
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"

CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
Why this approach:
  • Multi-stage builds reduce final image size by excluding build dependencies
  • Slim base images minimize attack surface and improve cold start times on Cloud Run
  • Non-root user improves security posture
  • Health check enables Cloud Run to monitor container status
  • Port 8080 is Cloud Run's convention (important for deployment)
Building and Testing Locally Test your Docker image locally before pushing to the cloud:
Bash
# Build image for your current architecture (ARM64 on M1/M2 Mac)
docker build -t my-app:latest .

# Test the container locally
docker run -p 8080:8080 \
  -e API_KEY="test-key" \
  -e NODE_ENV="production" \
  my-app:latest

# Verify your app is accessible
curl http://localhost:8080

# Check logs
docker logs <container-id>

Multi-Architecture Builds

Why Multi-Architecture Matters

When developing on macOS (ARM64 architecture), your Docker images are built for ARM64 by default. However, Google Cloud infrastructure may run on AMD64 (x86-64). The solution is multi-architecture builds that create images for both ARM64 and AMD64.

Building for Multiple Architectures

Docker Buildx enables building multi-architecture images. First, configure authentication:
Bash
# Check if buildx is available (Docker Desktop includes it)
docker buildx version

# Create a new builder instance for multi-platform builds
docker buildx create --name multiarch-builder --driver docker-container

# Set this builder as default
docker buildx use multiarch-builder

# Verify setup
docker buildx ls

# Set your GCP project (replace with your actual project ID)
gcloud config set project YOUR_GCP_PROJECT_ID

# Configure Docker authentication for GCR
gcloud auth configure-docker gcr.io
Now build and push for multiple architectures:
Bash
# Build for both ARM64 and AMD64, then push to GCR
docker buildx build \
  --platform linux/arm64,linux/amd64 \
  -t gcr.io/YOUR_GCP_PROJECT_ID/my-app:latest \
  --push \
  .

# Tag with semantic versioning for release management
docker buildx build \
  --platform linux/arm64,linux/amd64 \
  -t gcr.io/YOUR_GCP_PROJECT_ID/my-app:v1.0.0 \
  -t gcr.io/YOUR_GCP_PROJECT_ID/my-app:latest \
  --push \
  .

# Verify image in GCR
gcloud container images list --repository=gcr.io/YOUR_GCP_PROJECT_ID
gcloud container images describe gcr.io/YOUR_GCP_PROJECT_ID/my-app:v1.0.0
Flag Explanations:
  • --platform linux/arm64,linux/amd64: Builds for both Apple Silicon (ARM64) and standard cloud infrastructure (AMD64)
  • -t gcr.io/YOUR_GCP_PROJECT_ID/my-app:latest: Tags image for Google Container Registry
  • --push: Directly pushes to registry instead of saving locally

Cloud Run Deployment

Initial Setup
Bash
# Enable required APIs
gcloud services enable run.googleapis.com
gcloud services enable containerregistry.googleapis.com

# Set your default region
gcloud config set run/region us-central1

# Create a service account for Cloud Run (optional but recommended)
gcloud iam service-accounts create cloud-run-user \
  --display-name="Cloud Run Service Account"

# Grant necessary permissions
gcloud projects add-iam-policy-binding YOUR_GCP_PROJECT_ID \
  --member=serviceAccount:cloud-run-user@YOUR_GCP_PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/logging.logWriter
Deploy your containerized application to Cloud Run:
Bash
# Deploy with essential configuration
gcloud run deploy my-app \
  --image gcr.io/YOUR_GCP_PROJECT_ID/my-app:latest \
  --platform managed \
  --region us-central1 \
  --memory 1Gi \
  --cpu 1 \
  --timeout 60 \
  --max-instances 10 \
  --min-instances 1 \
  --allow-unauthenticated \
  --set-env-vars DATABASE_URL="your-db-url",API_KEY="your-api-key" \
  --service-account cloud-run-user@YOUR_GCP_PROJECT_ID.iam.gserviceaccount.com
Parameter Explanations:
  • --platform managed: Fully managed service (Google handles infrastructure)
  • --memory 1Gi: Allocate 1GB RAM (adjust based on your app needs)
  • --cpu 1: Allocate 1 vCPU
  • --timeout 60: Request timeout in seconds
  • --min-instances 1: Keep 1 warm instance (avoids cold starts; increases costs)
  • --allow-unauthenticated: Allow public traffic (remove for private services)
  • --set-env-vars: Pass environment variables to your application
  • --service-account: Use specific service account for better security
Updating and Monitoring After code changes, rebuild and redeploy:
Bash
# Push new image to GCR
docker buildx build \
  --platform linux/arm64,linux/amd64 \
  -t gcr.io/YOUR_GCP_PROJECT_ID/my-app:v1.0.1 \
  --push \
  .

# Update Cloud Run service to use new image
gcloud run deploy my-app \
  --image gcr.io/YOUR_GCP_PROJECT_ID/my-app:v1.0.1 \
  --region us-central1 \
  --update-env-vars DATABASE_URL="prod-db-url"

# Stream real-time logs
gcloud run logs read my-app --region us-central1 --follow

# Check deployment status
gcloud run services describe my-app --region us-central1

# View revisions
gcloud run revisions list --service my-app --region us-central1

Domain Configuration

Connecting Custom Domains Map your domain to Cloud Run and configure DNS:
Bash
# Add domain mappings to your Cloud Run service
gcloud run domain-mappings create \
  --service my-app \
  --domain yourdomain.com \
  --region us-central1

gcloud run domain-mappings create \
  --service my-app \
  --domain www.yourdomain.com \
  --region us-central1

# List mappings and get DNS records
gcloud run domain-mappings list
gcloud run domain-mappings describe yourdomain.com --region us-central1

# Verify DNS propagation
nslookup yourdomain.com
dig yourdomain.com +short

# Test HTTPS connectivity
curl -Iv https://yourdomain.com
Note: Google Cloud automatically provisions and manages SSL/TLS certificates. You don't need to install certificates manually.

CI/CD Pipeline

Automating Deployments Automate deployment when code is pushed to your repository:
Yaml
# Example: .github/workflows/deploy.yml (GitHub Actions)
name: Deploy to Cloud Run

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1
        with:
          project_id: ${{ secrets.GCP_PROJECT_ID }}
          service_account_key: ${{ secrets.GCP_SA_KEY }}

      - name: Configure Docker for GCR
        run: gcloud auth configure-docker gcr.io

      - name: Build and push image
        run: |
          docker buildx build \
            --platform linux/arm64,linux/amd64 \
            -t gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:${{ github.sha }} \
            -t gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest \
            --push \
            .

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy my-app \
            --image gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:${{ github.sha }} \
            --region us-central1

Troubleshooting

Architecture mismatch errors:
Bash
# Always use buildx with --platform flag
docker buildx build --platform linux/arm64,linux/amd64 -t my-app --push .
Cold start times:
Bash
# Keep containers warm with min-instances
gcloud run deploy my-app \
  --image gcr.io/YOUR_PROJECT/my-app:latest \
  --min-instances 1 \
  --region us-central1
Environment variables not applied:
Bash
# Always redeploy after changing environment variables
gcloud run deploy my-app \
  --image gcr.io/YOUR_PROJECT/my-app:latest \
  --update-env-vars KEY=VALUE \
  --region us-central1
Health check failures:
Bash
# Test locally first
curl -v http://localhost:8080/health

# Check Cloud Run logs
gcloud run logs read my-app --region us-central1 --limit 50

Production Checklist

Before going live:
  • ✅ Application tested thoroughly locally
  • ✅ Dockerfile optimized (multi-stage, minimal base images)
  • ✅ Multi-architecture build tested and working
  • ✅ Environment variables and secrets properly configured
  • ✅ Health check endpoint implemented
  • ✅ Cloud Run service deployed and responding
  • ✅ Custom domain mapped with DNS records configured
  • ✅ HTTPS working on both apex and www domains
  • ✅ CloudRun logs monitored and alerts configured
  • ✅ Appropriate min/max instance settings based on expected load

Key Takeaways

  • Develop locally first: Test thoroughly before containerization
  • Architecture matters: Multi-platform builds ensure your app runs on any infrastructure
  • Security first: Non-root users, minimal images, proper secrets management
  • Cloud Run conventions: Use port 8080, implement health checks, handle environment variables
  • Automate deployment: CI/CD pipelines reduce manual errors and enable faster iteration
  • Monitor production: Set up logging and alerts to catch issues early
Share this post:

Recent posts

Thumbnail of Mapping a Custom Domain to Google Cloud Run with Porkbun
Avatar
Hadi Rouhani
March 1, 2026
Mapping a Custom Domain to Google Cloud Run with PorkbunDevOps