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.
Before we start, make sure you have the following installed on your computer:
Once installed, open VS Code and install the **HashiCorp Terraform** extension from the marketplace for syntax highlighting and other helpers.
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).
This identity is what GitHub Actions will use to log in to Azure and build things on your behalf.
az login
az account show --query id --output tsv
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
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.
az group create --name "terraform-state-rg" --location "uksouth"
youruniquename
to something unique, e.g., n8nstate20250627
):
az storage account create --name "youruniquename" --resource-group "terraform-state-rg" --location "uksouth" --sku Standard_LRS
az storage container create --name "tfstate" --account-name "youruniquename"
az storage account keys list --resource-group "terraform-state-rg" --account-name "youruniquename" --query "[0].value" -o tsv
Now we'll create our GitHub repository and store all our secrets safely.
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. |
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.
.
├── .github/
│ └── workflows/
│ └── deploy.yml
├── .gitignore
└── terraform/
├── backend.tf
├── main.tf
├── outputs.tf
└── variables.tf
.gitignore
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
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
}
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"
}
}
terraform/outputs.tf
output "n8n_url" {
description = "The FQDN of the deployed n8n application."
value = "https://${azurerm_container_app.n8n_app.latest_revision_fqdn}"
}
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"
}
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.
.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
You have done all the hard work. Now for the easy part.
git add .
git commit -m "Initial commit for n8n infrastructure"
git push origin main
Terraform Apply
step. The n8n_url
will be printed in the outputs. Click that link to access your brand new, production-ready n8n instance!You now have a fully automated, secure, and robust n8n deployment that you can manage entirely through code.