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.
- GCP $300 Free Tier: Start with zero initial cost by utilizing the credits provided upon new account registration.
- Serverless Architecture (Cloud Run): Instead of keeping a server running 24/7, containers are only spun up when requests come in, dramatically reducing costs.
- 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
- Go to Search Console → Add Property → Enter your domain
- Copy the TXT record value provided by Google
- In Namecheap Advanced DNS, add a TXT Record with Host: @ and the copied value
- 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.