Deploying Production-Ready n8n on Azure: A Complete IaC & CI/CD Guide

This guide will walk you through deploying a robust, production-ready n8n instance on Microsoft Azure. We will use a modern Infrastructure as Code (IaC) and CI/CD approach, meaning every step is automated, repeatable, and managed through code.

By the end, you will have a cost-controlled, licensed n8n application that deploys automatically whenever you push changes to your code.

Our Toolkit

Part 1: Prerequisites - Your Developer Toolkit

Before we start, make sure you have the following installed on your computer:

  1. An Azure Account with an active subscription.
  2. A GitHub Account.
  3. Visual Studio Code
  4. Terraform (we recommend installing via Homebrew on macOS for the easiest setup).
  5. The Azure CLI

Once installed, open VS Code and install the **HashiCorp Terraform** extension from the marketplace for syntax highlighting and other helpers.

Part 2: The Foundation - Secure Access & Permanent Memory

This is the most critical part of a professional setup. We will create a secure identity for our automation (a Service Principal) and a permanent "memory" for Terraform so it never gets confused about what it has built (a Remote State Backend).

Step 2.1: Create a Secure "Robot" Identity in Azure

This identity is what GitHub Actions will use to log in to Azure and build things on your behalf.

  1. Open a terminal in VS Code and log in to Azure:
    az login
  2. Get your Azure Subscription ID, as you'll need it for the next command:
    az account show --query id --output tsv
  3. Create the Service Principal. Replace YOUR_SUBSCRIPTION_ID with the ID from the previous command.
    az ad sp create-for-rbac --name="n8n-github-sp" --role="Contributor" --scopes="/subscriptions/YOUR_SUBSCRIPTION_ID" --sdk-auth
  4. IMPORTANT: A block of JSON text containing secret credentials will be printed. Copy this entire JSON block immediately and save it somewhere temporarily and securely.

Step 2.2: Create Terraform's "Permanent Memory"

This prevents "resource already exists" errors by giving Terraform a dedicated place in Azure to store its memory (the state file). This is a one-time manual setup.

  1. Create a dedicated Resource Group for the state file:
    az group create --name "terraform-state-rg" --location "uksouth"
  2. Create a Storage Account (change youruniquename to something unique, e.g., n8nstate20250627):
    az storage account create --name "youruniquename" --resource-group "terraform-state-rg" --location "uksouth" --sku Standard_LRS
  3. Create a Blob Container:
    az storage container create --name "tfstate" --account-name "youruniquename"
  4. Get the Storage Access Key:
    az storage account keys list --resource-group "terraform-state-rg" --account-name "youruniquename" --query "[0].value" -o tsv
  5. IMPORTANT: Copy the long access key that this command outputs.

Part 3: The Secret Vault - Configuring GitHub

Now we'll create our GitHub repository and store all our secrets safely.

  1. Create a new, empty repository on GitHub.
  2. Go to your new repository's page, click Settings, then navigate to Secrets and variables > Actions.
  3. Click New repository secret and add the following secrets one by one:
    Secret Name Value
    AZURE_CREDENTIALS Paste the entire JSON block from Step 2.1.
    ARM_ACCESS_KEY Paste the Storage Access Key from Step 2.2.
    POSTGRES_ADMIN_PASSWORD Generate and paste a new, strong password.
    N8N_ENCRYPTION_KEY Generate and paste a random key (run openssl rand -base64 32 in your terminal).
    N8N_LICENSE_KEY Paste your n8n license key.

Part 4: The Master Blueprint - Your Terraform Code

Now, clone your repository to your computer, open it in VS Code, and create the following files and folders. For each file, copy and paste the provided code.

Project Structure:

.
├── .github/
│   └── workflows/
│       └── deploy.yml
├── .gitignore
└── terraform/
    ├── backend.tf
    ├── main.tf
    ├── outputs.tf
    └── variables.tf

File: .gitignore

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log

File: terraform/variables.tf

variable "location" {
  type        = string
  description = "The Azure region where all resources will be deployed."
  default     = "UK South"
}

variable "resource_group_name" {
  type        = string
  description = "The name for the Azure Resource Group."
  default     = "n8n-resourcegroup"
}

variable "postgres_admin_password" {
  type        = string
  description = "The password for the PostgreSQL admin user."
  sensitive   = true
}

variable "n8n_encryption_key" {
  type        = string
  description = "A secret key for n8n to encrypt sensitive data."
  sensitive   = true
}

variable "n8n_license_key" {
  type        = string
  description = "The license key for n8n."
  sensitive   = true
}

File: terraform/backend.tf

This tells Terraform where its permanent memory is. Remember to replace youruniquename.

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "youruniquename"
    container_name       = "tfstate"
    key                  = "n8n.prod.terraform.tfstate"
  }
}

File: terraform/outputs.tf

output "n8n_url" {
  description = "The FQDN of the deployed n8n application."
  value       = "https://${azurerm_container_app.n8n_app.latest_revision_fqdn}"
}

File: terraform/main.tf

This is the master blueprint for all our Azure resources.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "rg" {
  name     = var.resource_group_name
  location = var.location
}

resource "azurerm_container_app_environment" "cae" {
  name                = "n8n-environment"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_postgresql_flexible_server" "postgres" {
  name                   = "n8n-postgres-dbsrv"
  location               = azurerm_resource_group.rg.location
  resource_group_name    = azurerm_resource_group.rg.name
  sku_name               = "B_Standard_B1ms"
  version                = "13"
  administrator_login    = "n8nadmin"
  administrator_password = var.postgres_admin_password
  storage_mb             = 32768
  backup_retention_days  = 7
  public_network_access_enabled = true
}

resource "azurerm_postgresql_flexible_server_database" "n8ndb" {
  name      = "n8n_database"
  server_id = azurerm_postgresql_flexible_server.postgres.id
  charset   = "UTF8"
  collation = "en_US.utf8"
}

resource "azurerm_container_app" "n8n_app" {
  name                         = "n8n-container-app"
  container_app_environment_id = azurerm_container_app_environment.cae.id
  resource_group_name          = azurerm_resource_group.rg.name
  revision_mode                = "Single"

  template {
    min_replicas = 1
    max_replicas = 1

    container {
      name   = "n8n"
      image  = "n8nio/n8n:latest"
      cpu    = 0.5
      memory = "1.0Gi"

      env {
        name  = "DB_TYPE"
        value = "postgresdb"
      }
      env {
        name  = "DB_POSTGRESDB_HOST"
        value = "$${azurerm_postgresql_flexible_server.postgres.name}.postgres.database.azure.com"
      }
      env {
        name  = "DB_POSTGRESDB_DATABASE"
        value = azurerm_postgresql_flexible_server_database.n8ndb.name
      }
      env {
        name  = "DB_POSTGRESDB_USER"
        value = azurerm_postgresql_flexible_server.postgres.administrator_login
      }
      env {
        name        = "DB_POSTGRESDB_PASSWORD"
        secret_name = "postgres-password-secret"
      }
      env {
        name  = "DB_POSTGRESDB_SSL"
        value = "true"
      }
      env {
        name        = "N8N_ENCRYPTION_KEY"
        secret_name = "n8n-encryption-key-secret"
      }
      env {
        name        = "N8N_LICENSE_KEY"
        secret_name = "n8n-license-key-secret"
      }
      env {
        name  = "GENERIC_TIMEZONE"
        value = "Europe/London"
      }
    }
  }

  secret {
    name  = "postgres-password-secret"
    value = var.postgres_admin_password
  }
  secret {
    name  = "n8n-encryption-key-secret"
    value = var.n8n_encryption_key
  }
  secret {
    name  = "n8n-license-key-secret"
    value = var.n8n_license_key
  }

  ingress {
    external_enabled = true
    target_port      = 5678
    transport        = "auto"

    traffic_weight {
      percentage      = 100
      latest_revision = true
    }
  }
}

resource "azurerm_postgresql_flexible_server_firewall_rule" "allow_azure_services" {
  name                = "allow-azure-services"
  server_id           = azurerm_postgresql_flexible_server.postgres.id
  start_ip_address    = "0.0.0.0"
  end_ip_address      = "0.0.0.0"
}

Part 5: The Automation Engine - The GitHub Workflow

This file is the instruction manual for our robot builder. It tells it how to log in, use the permanent memory, and build the resources.

File: .github/workflows/deploy.yml

name: Build and Deploy n8n Home

on:
  push:
    branches:
      - main

jobs:
  deploy:
    name: 'Terraform Deployment'
    runs-on: ubuntu-latest
    environment: production

    defaults:
      run:
        shell: bash
        working-directory: ./terraform

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: $${{ secrets.AZURE_CREDENTIALS }}

      - name: Terraform Init
        run: terraform init -input=false
        env:
          ARM_ACCESS_KEY: $${{ secrets.ARM_ACCESS_KEY }}

      - name: Terraform Plan
        run: terraform plan -out=tfplan
        env:
          TF_VAR_postgres_admin_password: $${{ secrets.POSTGRES_ADMIN_PASSWORD }}
          TF_VAR_n8n_encryption_key: $${{ secrets.N8N_ENCRYPTION_KEY }}
          TF_VAR_n8n_license_key: $${{ secrets.N8N_LICENSE_KEY }}

      - name: Terraform Apply
        run: terraform apply -auto-approve tfplan

Part 6: Deployment - Bringing It All to Life

You have done all the hard work. Now for the easy part.

  1. In your VS Code terminal, save all your work to GitHub with these three commands:
    git add .
    git commit -m "Initial commit for n8n infrastructure"
    git push origin main
  2. Go to your GitHub repository and click the Actions tab. You will see your workflow running. It will take 5-10 minutes to build everything for the first time.
  3. When it's done, click the job and look at the Terraform Apply step. The n8n_url will be printed in the outputs. Click that link to access your brand new, production-ready n8n instance!

Congratulations!

You now have a fully automated, secure, and robust n8n deployment that you can manage entirely through code.