Last updated: Aug 4, 2025, 11:26 AM UTC

n8n Self-Hosted Deployment Guide: Zero-Cost Automation Engine

Status: Production Deployment Guide
Research Focus: Self-Hosted n8n on Google Cloud Run for Maximum Cost Efficiency
Verified: Based on production patterns and real deployments


Executive Summary

n8n is the automation heart of our zero-cost architecture. By self-hosting n8n on Google Cloud Run instead of using n8n Cloud, we achieve unlimited workflow executions at ~$8/month base cost. This guide provides complete implementation details for deploying, optimizing, and monitoring a production-ready n8n instance that scales to zero when idle.

Why Self-Hosted n8n on Cloud Run

n8n Architecture Decision Matrix

graph TD subgraph "n8n Cloud (Rejected)" A[Fixed Monthly Cost] --> B[$50-500/month] B --> C[Execution Limits] C --> D[Vendor Lock-in] end subgraph "Self-Hosted VM (Rejected)" E[Always-On Server] --> F[$40+/month] F --> G[Manual Scaling] G --> H[Maintenance Overhead] end subgraph "Cloud Run (Chosen)" I[Scale to Zero] --> J[~$8/month base] J --> K[Unlimited Executions] K --> L[Auto-scaling] end style D fill:#ffcdd2 style H fill:#ffcdd2 style L fill:#c8e6c9

Key Benefits of Our Approach

Aspect n8n Cloud VM Deployment Cloud Run
Fixed Costs $50-500/month $40+/month $8/month
Executions Limited by plan Unlimited Unlimited
Scaling Automatic Manual Auto + Scale-to-0
Maintenance None High Minimal
Cold Starts None None 3-5 seconds

Architecture Overview

1. n8n in the NudgeCampaign Stack

n8n's Role: The Automation Orchestrator

Integration Points:

  • Triggers: Webhooks from Next.js API
  • Data Source: Direct Cloud SQL access
  • Email Delivery: Postmark API integration
  • Monitoring: Google Cloud Logging & Monitoring
sequenceDiagram participant UI as Next.js App participant API as Cloud Run API participant n8n as n8n (Cloud Run) participant DB as Cloud SQL participant PM as Postmark UI->>API: Create Campaign API->>DB: Store Campaign Data API->>n8n: Trigger Workflow n8n->>DB: Fetch Recipients n8n->>n8n: Personalize Content n8n->>PM: Send Batch (500 emails) n8n->>DB: Update Send Status Note over n8n: Scales to 0 when idle Note over PM: Usage-based pricing

2. Core Components

# n8n Cloud Run Deployment Architecture
services:
  n8n-app:
    image: n8nio/n8n:latest
    platform: Cloud Run
    scaling:
      min_instances: 0  # Scale to zero
      max_instances: 10
      concurrent_requests: 10
    
  database:
    type: Cloud SQL
    tier: db-f1-micro  # $8/month
    connections: 25
    
  storage:
    workflows: PostgreSQL
    credentials: Google Secret Manager
    executions: PostgreSQL (with retention)

Step-by-Step Deployment

Phase 1: Environment Setup

Prerequisites

  • Google Cloud Project with billing enabled
  • Cloud SQL instance (existing $8/month db-f1-micro)
  • Domain for n8n interface (subdomain recommended)
  • Postmark account with API token

1.1 Create n8n Database

-- Connect to your existing Cloud SQL instance
CREATE DATABASE n8n_prod;
CREATE USER n8n_user WITH PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE n8n_prod TO n8n_user;

-- Switch to n8n database
\c n8n_prod;

-- Grant schema permissions
GRANT ALL ON SCHEMA public TO n8n_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO n8n_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO n8n_user;

1.2 Configure Secret Manager

# Store sensitive configuration in Secret Manager
gcloud secrets create n8n-db-password --data-file=- <<< "your-secure-password"
gcloud secrets create n8n-encryption-key --data-file=- <<< "$(openssl rand -base64 32)"
gcloud secrets create postmark-token --data-file=- <<< "your-postmark-server-token"

# Grant Cloud Run access to secrets
gcloud secrets add-iam-policy-binding n8n-db-password \
  --member="serviceAccount:your-cloudrun-sa@project.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Phase 2: Container Configuration

2.1 Create Dockerfile

# Dockerfile for n8n Cloud Run deployment
FROM n8nio/n8n:latest

# Install additional nodes if needed
# RUN npm install -g n8n-nodes-postmark

# Set ownership for security
USER node

# Health check for Cloud Run
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:5678/healthz || exit 1

# Expose port
EXPOSE 5678

# Use exec form for proper signal handling
CMD ["n8n", "start"]

2.2 Environment Configuration

# cloudbuild.yaml for automated deployment
steps:
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'gcr.io/$PROJECT_ID/n8n:$BUILD_ID', '.']
  
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'gcr.io/$PROJECT_ID/n8n:$BUILD_ID']
  
  - name: 'gcr.io/cloud-builders/gcloud'
    args:
      - 'run'
      - 'deploy'
      - 'n8n-automation'
      - '--image=gcr.io/$PROJECT_ID/n8n:$BUILD_ID'
      - '--region=us-central1'
      - '--platform=managed'
      - '--allow-unauthenticated'
      - '--port=5678'
      - '--memory=1Gi'
      - '--cpu=1'
      - '--min-instances=0'
      - '--max-instances=10'
      - '--set-env-vars=NODE_ENV=production'
      - '--set-secrets=DB_POSTGRESDB_PASSWORD=n8n-db-password:latest'
      - '--set-secrets=N8N_ENCRYPTION_KEY=n8n-encryption-key:latest'

Phase 3: Cloud Run Service Definition

3.1 Complete Service Configuration

# service.yaml - Complete Cloud Run configuration
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: n8n-automation
  annotations:
    run.googleapis.com/ingress: all
    run.googleapis.com/launch-stage: GA
spec:
  template:
    metadata:
      annotations:
        # Scaling configuration
        autoscaling.knative.dev/minScale: "0"
        autoscaling.knative.dev/maxScale: "10"
        
        # Performance optimization
        run.googleapis.com/cpu-throttling: "true"
        run.googleapis.com/startup-cpu-boost: "true"
        run.googleapis.com/execution-environment: gen2
        
        # Connection timeout
        run.googleapis.com/timeout: "900s"  # 15 minutes for long workflows
    spec:
      containerConcurrency: 10  # Conservative for stability
      serviceAccountName: n8n-service-account
      containers:
      - image: gcr.io/your-project/n8n:latest
        ports:
        - containerPort: 5678
        resources:
          limits:
            cpu: "1"
            memory: "1Gi"
          requests:
            cpu: "0.1"  # Minimal for cold starts
            memory: "256Mi"
        env:
        # Core n8n configuration
        - name: NODE_ENV
          value: "production"
        - name: N8N_PROTOCOL
          value: "https"
        - name: N8N_HOST
          value: "n8n.nudgecampaign.com"
        - name: N8N_PORT
          value: "5678"
        - name: WEBHOOK_URL
          value: "https://n8n.nudgecampaign.com"
        
        # Database configuration
        - name: DB_TYPE
          value: "postgresdb"
        - name: DB_POSTGRESDB_HOST
          value: "your-cloud-sql-ip"
        - name: DB_POSTGRESDB_PORT
          value: "5432"
        - name: DB_POSTGRESDB_DATABASE
          value: "n8n_prod"
        - name: DB_POSTGRESDB_USER
          value: "n8n_user"
        
        # Execution settings
        - name: EXECUTIONS_PROCESS
          value: "main"  # Run in main process for Cloud Run
        - name: EXECUTIONS_TIMEOUT
          value: "300"   # 5 minutes max per workflow
        - name: EXECUTIONS_DATA_SAVE_ON_ERROR
          value: "all"
        - name: EXECUTIONS_DATA_SAVE_ON_SUCCESS
          value: "all"
        - name: EXECUTIONS_DATA_PRUNE
          value: "true"
        - name: EXECUTIONS_DATA_MAX_AGE
          value: "168"   # 7 days retention
        
        # Performance tuning
        - name: N8N_PAYLOAD_SIZE_MAX
          value: "16"    # 16MB max payload
        - name: NODE_OPTIONS
          value: "--max-old-space-size=1024"
        
        # Security
        - name: N8N_BASIC_AUTH_ACTIVE
          value: "true"
        - name: N8N_BASIC_AUTH_USER
          value: "admin"
        
        # Secrets from Secret Manager
        - name: DB_POSTGRESDB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: n8n-db-password
              key: latest
        - name: N8N_ENCRYPTION_KEY
          valueFrom:
            secretKeyRef:
              name: n8n-encryption-key
              key: latest
        - name: N8N_BASIC_AUTH_PASSWORD
          valueFrom:
            secretKeyRef:
              name: n8n-admin-password
              key: latest

3.2 Deploy with gcloud

# Deploy the service
gcloud run services replace service.yaml --region=us-central1

# Set up custom domain
gcloud run domain-mappings create \
  --service=n8n-automation \
  --domain=n8n.nudgecampaign.com \
  --region=us-central1

Production Optimization

1. Database Connection Optimization

πŸ—„οΈ Efficient Database Usage

n8n can be database-intensive. Optimize connections to minimize the Cloud SQL load and costs.

# Additional environment variables for database optimization
env:
  # Connection pooling
  - name: DB_POSTGRESDB_POOL_SIZE
    value: "5"  # Conservative pool size for serverless
  
  # Connection timeouts
  - name: DB_POSTGRESDB_POOL_CONNECTION_TIMEOUT_MILLIS
    value: "3000"
  - name: DB_POSTGRESDB_POOL_IDLE_TIMEOUT_MILLIS
    value: "10000"
  
  # SSL configuration
  - name: DB_POSTGRESDB_SSL_CA
    value: "/etc/ssl/certs/server-ca.pem"
  - name: DB_POSTGRESDB_SSL_CERT
    value: "/etc/ssl/certs/client-cert.pem"
  - name: DB_POSTGRESDB_SSL_KEY
    value: "/etc/ssl/private/client-key.pem"
  - name: DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED
    value: "false"

2. Workflow Performance Optimization

2.1 Efficient Workflow Patterns

// Example: Optimized bulk email sending workflow
{
  "name": "Bulk Email Campaign - Optimized",
  "nodes": [
    {
      "name": "Campaign Trigger",
      "type": "webhook",
      "parameters": {
        "path": "campaign-send-v3",
        "responseMode": "immediately"
      }
    },
    {
      "name": "Get Recipients Batch",
      "type": "postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": `
          SELECT email, first_name, last_name, preferences
          FROM contacts 
          WHERE campaign_id = $1 
          AND status = 'active'
          AND email_verified = true
          LIMIT 500
        `,
        "additionalFields": {
          "queryParams": "={{ $json.campaignId }}"
        }
      }
    },
    {
      "name": "Batch Processor",
      "type": "splitInBatches",
      "parameters": {
        "batchSize": 500  // Postmark's maximum
      }
    },
    {
      "name": "Personalize Content",
      "type": "code",
      "parameters": {
        "language": "javascript",
        "code": `
          // Efficient personalization without external API calls
          const template = $items[0].json.template;
          const recipients = $items[1].json;
          
          return recipients.map(recipient => ({
            json: {
              From: 'hello@nudgecampaign.com',
              To: recipient.email,
              Subject: template.subject.replace('{{firstName}}', recipient.first_name || 'there'),
              HtmlBody: personalizeTemplate(template.html, recipient),
              TextBody: personalizeTemplate(template.text, recipient),
              MessageStream: "marketing",
              Tag: \`campaign-\${$items[0].json.campaignId}\`,
              Metadata: {
                campaignId: $items[0].json.campaignId,
                contactId: recipient.id,
                batchId: $items[0].json.batchId
              }
            }
          }));
          
          function personalizeTemplate(template, recipient) {
            return template
              .replace(/{{firstName}}/g, recipient.first_name || 'there')
              .replace(/{{lastName}}/g, recipient.last_name || '')
              .replace(/{{email}}/g, recipient.email);
          }
        `
      }
    },
    {
      "name": "Send via Postmark",
      "type": "postmark",
      "parameters": {
        "operation": "sendBatch",
        "messages": "={{ $json }}"
      }
    },
    {
      "name": "Update Status",
      "type": "postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": `
          UPDATE contacts 
          SET 
            last_email_sent = NOW(),
            email_count = email_count + 1,
            campaign_status = 'sent'
          WHERE id = ANY($1)
        `,
        "additionalFields": {
          "queryParams": "={{ $json.map(item => item.Metadata.contactId) }}"
        }
      }
    }
  ]
}

2.2 Error Handling & Retry Logic

// Robust error handling node
{
  "name": "Error Handler",
  "type": "code",
  "parameters": {
    "language": "javascript",
    "code": `
      const error = $items[0].json;
      
      // Classify error types
      const errorType = classifyError(error);
      
      switch (errorType) {
        case 'RATE_LIMIT':
          // Exponential backoff
          const delay = Math.min(300000, Math.pow(2, error.retryCount || 0) * 1000);
          return [{ json: { action: 'retry', delay, error } }];
          
        case 'TEMPORARY':
          // Simple retry
          return [{ json: { action: 'retry', delay: 30000, error } }];
          
        case 'PERMANENT':
          // Log and skip
          return [{ json: { action: 'skip', reason: error.message } }];
          
        default:
          // Unknown error - retry once
          if ((error.retryCount || 0) < 1) {
            return [{ json: { action: 'retry', delay: 10000, error } }];
          } else {
            return [{ json: { action: 'fail', error } }];
          }
      }
      
      function classifyError(error) {
        if (error.message.includes('rate limit')) return 'RATE_LIMIT';
        if (error.message.includes('timeout')) return 'TEMPORARY';
        if (error.message.includes('invalid email')) return 'PERMANENT';
        return 'UNKNOWN';
      }
    `
  }
}

3. Cold Start Optimization

# Dockerfile optimization for faster cold starts
FROM n8nio/n8n:latest

# Pre-warm node modules
RUN npm install --production --silent

# Minimize layers
COPY --chown=node:node . /home/node/.n8n/

# Use multi-stage build for smaller image
FROM node:18-alpine
COPY --from=0 /usr/local/lib/node_modules/n8n /usr/local/lib/node_modules/n8n
COPY --from=0 /usr/local/bin/n8n /usr/local/bin/n8n

USER node
CMD ["n8n", "start"]

Monitoring & Observability

1. Cloud Run Metrics

Key Metrics to Monitor

  • Cold Start Frequency: Should be <5% of requests
  • Execution Time: Average workflow completion time
  • Error Rate: Failed workflows percentage
  • Database Connections: Active connection count
  • Memory Usage: Container memory utilization
# Cloud Monitoring dashboard configuration
resources:
  - name: "n8n-dashboard"
    type: monitoring.dashboard
    properties:
      displayName: "n8n Automation Metrics"
      mosaicLayout:
        tiles:
        - width: 6
          height: 4
          widget:
            title: "Cold Start Rate"
            scorecard:
              timeSeriesQuery:
                timeSeriesFilter:
                  filter: 'resource.type="cloud_run_revision"'
                  metric: 'run.googleapis.com/container/startup_latencies'
        - width: 6
          height: 4
          widget:
            title: "Active Instances"
            scorecard:
              timeSeriesQuery:
                timeSeriesFilter:
                  filter: 'resource.type="cloud_run_revision"'
                  metric: 'run.googleapis.com/container/instance_count'

2. Custom Application Metrics

// Add to n8n startup script for custom metrics
const { MeterProvider } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { PrometheusExporterWrapper } = require('@opentelemetry/exporter-prometheus');

const meterProvider = new MeterProvider({
  resource: new Resource({
    'service.name': 'n8n-automation',
    'service.version': process.env.N8N_VERSION
  })
});

const meter = meterProvider.getMeter('n8n-custom');

// Workflow execution metrics
const workflowExecutions = meter.createCounter('n8n_workflow_executions', {
  description: 'Number of workflow executions'
});

const workflowDuration = meter.createHistogram('n8n_workflow_duration', {
  description: 'Workflow execution duration in milliseconds'
});

// Export metrics for Cloud Monitoring
const exporter = new PrometheusExporterWrapper({
  port: 9090
});
meterProvider.addMetricReader(exporter);

3. Alerting Configuration

# Cloud Monitoring alert policies
alerting:
  policies:
  - displayName: "n8n High Error Rate"
    conditions:
    - displayName: "Error rate > 5%"
      conditionThreshold:
        filter: 'resource.type="cloud_run_revision" AND resource.label.service_name="n8n-automation"'
        comparison: COMPARISON_GREATER_THAN
        thresholdValue: 0.05
        duration: 300s
    notificationChannels:
    - "projects/your-project/notificationChannels/slack-alerts"
    
  - displayName: "n8n Container Memory High"
    conditions:
    - displayName: "Memory usage > 80%"
      conditionThreshold:
        filter: 'resource.type="cloud_run_revision"'
        metric: 'run.googleapis.com/container/memory/utilizations'
        comparison: COMPARISON_GREATER_THAN
        thresholdValue: 0.8

Cost Optimization Strategies

1. Execution Data Retention

# Optimize execution history storage
env:
- name: EXECUTIONS_DATA_PRUNE
  value: "true"
- name: EXECUTIONS_DATA_MAX_AGE
  value: "72"  # 3 days for production
- name: EXECUTIONS_DATA_SAVE_ON_SUCCESS
  value: "none"  # Only save errors
- name: EXECUTIONS_DATA_SAVE_ON_ERROR
  value: "all"   # Keep errors for debugging

2. Efficient Resource Usage

# Right-size containers based on actual usage
gcloud run services update n8n-automation \
  --memory=512Mi \
  --cpu=0.5 \
  --concurrency=5 \
  --max-instances=5 \
  --region=us-central1

3. Database Query Optimization

-- Optimize n8n database with indexes
CREATE INDEX CONCURRENTLY idx_executions_finished_at 
ON executions_entity (finished_at) 
WHERE finished_at IS NOT NULL;

CREATE INDEX CONCURRENTLY idx_executions_workflow_id 
ON executions_entity (workflow_id, finished_at);

-- Clean up old executions automatically
CREATE OR REPLACE FUNCTION cleanup_old_executions()
RETURNS void AS $
BEGIN
  DELETE FROM executions_entity 
  WHERE finished_at < NOW() - INTERVAL '7 days';
END;
$ LANGUAGE plpgsql;

-- Schedule cleanup (run from cron job)
SELECT cron.schedule('cleanup-executions', '0 2 * * *', 'SELECT cleanup_old_executions();');

Production Checklist

Pre-Launch

  • Database Setup: n8n database created with proper permissions
  • Secrets Management: All credentials stored in Secret Manager
  • SSL Configuration: HTTPS enabled with custom domain
  • Basic Auth: Admin interface secured
  • Resource Limits: CPU/Memory configured appropriately
  • Health Checks: Container health monitoring enabled

Monitoring

  • Alerting: Error rate and performance alerts configured
  • Logging: Structured logging enabled
  • Metrics: Custom application metrics implemented
  • Dashboard: Cloud Monitoring dashboard created
  • Budget Alerts: Cost monitoring alerts set up

Optimization

  • Cold Starts: Optimized for sub-5-second startup
  • Database: Connection pooling configured
  • Executions: Retention policy implemented
  • Workflows: Batch processing patterns adopted
  • Error Handling: Robust retry logic implemented

Conclusion

Self-hosting n8n on Google Cloud Run provides the perfect balance of cost efficiency and operational simplicity:

~$8/month base cost (vs $50-500 for n8n Cloud)
Unlimited workflow executions
Auto-scaling from 0 to 10 instances
Production-ready reliability
Complete control over configuration

Result: Enterprise-grade automation at indie hacker prices

This deployment strategy allows n8n to serve as the automation brain of your email marketing platform while maintaining the zero-fixed-cost philosophy that makes your business model sustainable from day one.


Related Documents


n8n deployment guide based on production implementations and 2025 Google Cloud pricing. Last updated: 2025-07-28