Building Your Own Tech Blog on GCP Free Credits β€” Wagtail + Cloud Run + Cloud SQL + Custom Domain

A step-by-step guide to building a production-ready blog from scratch using GCP's $300 free credits. Covers Wagtail CMS setup, Cloud SQL Auth Proxy configuration, TinyMCE editor integration, Docker containerization, Cloud Run serverless deployment, and custom domain configuration with automatic SSL.

April 10, 2026  Β·  7 min read

Building Your Own Tech Blog on GCP Free Credits

A complete guide from Wagtail CMS setup to Cloud Run deployment and custom domain configuration

This post documents the entire process of building isupernova.io from scratch using GCP's $300 free credits. It covers everything from infrastructure design, Wagtail CMS installation, Cloud SQL connection, TinyMCE editor integration, Cloud Run deployment, to custom domain configuration.

Tech Stack

Category Technology Description
CMS / Framework Wagtail 7.3 + Django 6.0 Python-based CMS
Database Cloud SQL (PostgreSQL 15) GCP managed DB
Deployment Cloud Run GCP serverless container
Container Docker Image build and deployment
Static Files WhiteNoise Django static file serving
Editor django-tinymce Rich text editor with HTML source editing
Dev Environment GCP Cloud Shell Browser-based terminal
Domain Namecheap isupernova.io

Step 0: Strategic Planning - GCP Free Credits and Infrastructure Design

Strategy

The architecture was designed to minimize cost while achieving enterprise-grade performance.

  1. GCP $300 Free Tier: Start with zero initial cost by utilizing the credits provided upon new account registration.
  2. Serverless Architecture (Cloud Run): Instead of keeping a server running 24/7, containers are only spun up when requests come in, dramatically reducing costs.
  3. Managed DB (Cloud SQL): Use Google's managed PostgreSQL service for data stability and reliability.

Step 1: Development Environment - Cloud Shell

Environment Setup

Develop entirely from a browser without any local setup.

  • Cloud Shell Editor: A Visual Studio Code (Web)-based editor that runs in the browser.
  • How to access: Click the terminal icon at the top of GCP Console → Click "Open Editor"
  • Web Preview: Start the server on port 8080, then use the "Web Preview" button in the top right to see results in real time.

πŸ’‘ Cloud Shell automatically terminates after 20 minutes of inactivity. Register the Cloud SQL Auth Proxy in ~/.bashrc so the DB connection is automatically restored when the session restarts.


Step 2: CMS Selection - Why Wagtail

Technology Choice

Criteria WordPress Django (Pure) Wagtail (Selected)
Flexibility Low (plugin-dependent) Very High (full custom code) High (easy customization)
Security Vulnerable (frequent updates) High (Python-based) High (Django-based security)
Ease of Management Moderate Low (requires full development) Excellent (professional CMS UI)
Cost Paid hosting required High dev resource cost Optimizable for Cloud Run

Step 3: Wagtail Installation and Initial Setup

Installation

Create Project

pip install wagtail==7.3rc1
wagtail start mysite
cd mysite

requirements.txt

Django>=6,<6.1
wagtail==7.3rc1
psycopg2-binary==2.9.9
django-tinymce==4.1.0
setuptools>=65.5.0
gunicorn==21.2.0
whitenoise==6.6.0

settings/base.py - Base Configuration

INSTALLED_APPS = [
    "django.contrib.postgres",
    "home",
    "search",
    "wagtail.contrib.forms",
    "wagtail.contrib.redirects",
    "wagtail.embeds",
    "wagtail.sites",
    "wagtail.users",
    "wagtail.snippets",
    "wagtail.documents",
    "wagtail.images",
    "wagtail.search",
    "wagtail.admin",
    "wagtail",
    "modelcluster",
    "taggit",
    "django_filters",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "tinymce",  # Add TinyMCE
]

Step 4: Cloud SQL Auth Proxy Configuration

DB Connection

Register the Auth Proxy in ~/.bashrc so it automatically reconnects even when the Cloud Shell session is interrupted.

# Download Auth Proxy
cd ~
wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy
chmod +x cloud_sql_proxy

# Run in background
./cloud_sql_proxy -instances=[PROJECT_ID]:[REGION]:[INSTANCE_NAME]=tcp:5432 &

# Register in .bashrc for auto-start on session restart
echo '~/cloud_sql_proxy -instances=[PROJECT_ID]:[REGION]:[INSTANCE_NAME]=tcp:5432 &' >> ~/.bashrc

settings/base.py - DB Configuration (Development)

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "postgres",
        "USER": "postgres",
        "PASSWORD": "your-password",
        "HOST": "127.0.0.1",  # Auth Proxy local connection
        "PORT": "5432",
    }
}

Step 5: TinyMCE Editor Integration

Editor

Wagtail's default editor (Draftail) does not support HTML source editing. Replacing it with TinyMCE enables direct HTML source editing, which is essential for pasting custom HTML content.

settings/base.py - TinyMCE Configuration

TINYMCE_DEFAULT_CONFIG = {
    'height': 600,
    'plugins': 'code link lists table image paste',
    'toolbar': (
        'undo redo | styleselect | bold italic underline | '
        'alignleft aligncenter alignright | '
        'bullist numlist | link image table | code'
    ),
    'menubar': True,
    'code_dialog_height': 600,
    'code_dialog_width': 900,
}

urls.py - Add TinyMCE URL

urlpatterns = [
    path("django-admin/", admin.site.urls),
    path("admin/", include(wagtailadmin_urls)),
    path("documents/", include(wagtaildocs_urls)),
    path("search/", search_views.search, name="search"),
    path("tinymce/", include("tinymce.urls")),  # Add this
]

models.py - Replace RichTextField with TextField + TinyMCE

from django.db import models
from wagtail.models import Page
from wagtail.admin.panels import FieldPanel
from tinymce.widgets import TinyMCE

class BlogPage(Page):
    date = models.DateField("Date")
    intro = models.TextField("Introduction")
    body = models.TextField("Body", blank=True)  # RichTextField → TextField

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', widget=TinyMCE()),
    ]

blog_page.html - Use safe filter instead of richtext

{{ page.body|safe }}

Step 6: Dockerization

Containerization

Dockerfile

FROM python:3.12-slim-bookworm

RUN useradd wagtail

EXPOSE 8000

ENV PYTHONUNBUFFERED=1 \
    PORT=8000

RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \
    build-essential \
    libpq-dev \
    libmariadb-dev \
    libjpeg62-turbo-dev \
    zlib1g-dev \
    libwebp-dev \
 && rm -rf /var/lib/apt/lists/*

RUN pip install "gunicorn==21.2.0"

COPY requirements.txt /
RUN pip install -r /requirements.txt

WORKDIR /app
RUN chown wagtail:wagtail /app
COPY --chown=wagtail:wagtail . .
USER wagtail

RUN python manage.py collectstatic --noinput --clear

CMD set -xe; python manage.py migrate --noinput; gunicorn mysite.wsgi:application

Step 7: Cloud Run Deployment

Deployment

settings/production.py

from .base import *
import os

DEBUG = False

SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key')

# Manage ALLOWED_HOSTS via environment variable
# so it can be updated without rebuilding the container
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')

CSRF_TRUSTED_ORIGINS = [
    'https://yourdomain.io',
    'https://www.yourdomain.io',
    'https://*.run.app',
]

# WhiteNoise Middleware for static file serving
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # Add this
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]

# Cloud Run - Cloud SQL Unix Socket connection
# Use Unix Socket path instead of IP address
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'your-password',
        'HOST': '/cloudsql/[PROJECT_ID]:[REGION]:[INSTANCE_NAME]',
        # Do NOT include PORT when using Unix Socket
    }
}

# WhiteNoise static files configuration
STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage",
    },
}

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {'console': {'class': 'logging.StreamHandler'}},
    'root': {'handlers': ['console'], 'level': 'WARNING'},
}

try:
    from .local import *
except ImportError:
    pass

Create Artifact Registry Repository

gcloud services enable artifactregistry.googleapis.com

gcloud artifacts repositories create [REPO_NAME] \
    --repository-format=docker \
    --location=us-central1

Grant Cloud SQL Permissions

gcloud projects add-iam-policy-binding [PROJECT_ID] \
    --member="serviceAccount:[PROJECT_NUMBER]-compute@developer.gserviceaccount.com" \
    --role="roles/cloudsql.client"

Build and Deploy

# Create env.yaml (resolves comma issue with --set-env-vars)
cat > ~/env.yaml << 'EOF'
DJANGO_SETTINGS_MODULE: mysite.settings.production
SECRET_KEY: your-secret-key
ALLOWED_HOSTS: "yourdomain.io,www.yourdomain.io,your-cloudrun-url.run.app"
EOF

# Build Docker image
gcloud builds submit --tag us-central1-docker.pkg.dev/[PROJECT_ID]/[REPO_NAME]/[APP_NAME]

# Deploy to Cloud Run
gcloud run deploy [APP_NAME] \
    --image us-central1-docker.pkg.dev/[PROJECT_ID]/[REPO_NAME]/[APP_NAME] \
    --platform managed \
    --region us-central1 \
    --allow-unauthenticated \
    --env-vars-file ~/env.yaml \
    --add-cloudsql-instances [PROJECT_ID]:[REGION]:[INSTANCE_NAME] \
    --port 8000

πŸ’‘ Key Point: When connecting Cloud Run to Cloud SQL, you must use Unix Socket instead of an IP address. Set HOST to /cloudsql/[PROJECT_ID]:[REGION]:[INSTANCE_NAME] and omit the PORT field entirely.


Step 8: Custom Domain Configuration

Domain

1. Verify Domain Ownership via Google Search Console

  1. Go to Search Console → Add Property → Enter your domain
  2. Copy the TXT record value provided by Google
  3. In Namecheap Advanced DNS, add a TXT Record with Host: @ and the copied value
  4. Return to Search Console and click CHECK to complete verification

2. Map Domain to Cloud Run

gcloud beta run domain-mappings create \
    --service [SERVICE_NAME] \
    --domain yourdomain.io \
    --region us-central1

3. Configure Namecheap DNS Records

Add the A records and AAAA records provided by Cloud Run to Namecheap Advanced DNS. Make sure to delete any existing URL Redirect Records to avoid conflicts.

# A Records (4 entries, Host: @)
216.239.32.21
216.239.34.21
216.239.36.21
216.239.38.21

# AAAA Records (4 entries, Host: @)
2001:4860:4802:32::15
2001:4860:4802:34::15
2001:4860:4802:36::15
2001:4860:4802:38::15

⚠️ After DNS propagation, SSL certificate issuance takes approximately 30 minutes to 1 hour. Seeing ERR_CONNECTION_CLOSED before the certificate is issued is expected behavior.


Final Architecture

User Browser
      ↓
yourdomain.io (Namecheap DNS → Google IP)
      ↓
Google Cloud Run (Serverless Container)
      ↓ Unix Socket
Cloud SQL PostgreSQL (Managed DB)

Static Files : WhiteNoise (served by Django)
SSL          : Auto-provisioned by Cloud Run
Container    : Artifact Registry
Dev Env      : GCP Cloud Shell

Key Takeaways

1. Cloud Run + Cloud SQL delivers near-zero cost when there is no traffic.
2. Cloud SQL connection from Cloud Run must use Unix Socket, not an IP address.
3. WhiteNoise handles static files simply without needing a separate GCS bucket.
4. Managing ALLOWED_HOSTS via environment variables allows updates without rebuilding.
5. Use --env-vars-file instead of --set-env-vars when ALLOWED_HOSTS contains commas.