--- title: Vendor-specific instructions sidebar_position: 3 sidebar_custom_props: icon: TbCloud --- :::info This document is maintained by the community. It might contain issues. Feel free to join our discord if you need assistance. ::: ## Render [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/twentyhq/twenty) ## RepoCloud [![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=259) ## Azure Container Apps ### About Hosts Twenty CRM using Azure Container Apps. The solution provisions file shares, a container apps environment with three containers, and a log analytics workspace. The file shares are used to store uploaded images and files through the UI, and to store database backups. ### Prerequisites - Terraform installed https://developer.hashicorp.com/terraform/install - An Azure subscription with permissions to create resources ### Step by step instructions: 1. Create a new folder and copy all the files from below 2. Run `terraform init` 3. Run `terraform plan -out tfplan` 4. Run `terraform apply tfplan` 5. Connect to server `az containerapp exec --name twenty-server -g twenty-crm-rg` 6. Initialize the database from the server `yarn database:init:prod` 7. Go to https://your-twenty-front-fqdn - located in the portal #### Production docker containers This uses the prebuilt images found on [docker hub](https://hub.docker.com/r/twentycrm/). #### Environment Variables - Is set in respective tf-files - See docs [Setup Environment Variables](https://docs.twenty.com/start/self-hosting/) for usage - After deployment you could can set `IS_SIGN_UP_DISABLED=true` (and run `terraform plan/apply` again) to disable new workspaces from being created #### Security and networking - Container `twenty-db` accepts only ingress TCP traffic from other containers in the environment. No external ingress traffic allowed - Container `twenty-server` accepts external traffic over HTTPS - Container `twenty-front` accepts external traffic over HTTPS It´s highly recommended to enable [built-in authentication](https://learn.microsoft.com/en-us/azure/container-apps/authentication) for `twenty-front` using one of the supported providers. Use the [custom domain](https://learn.microsoft.com/en-us/azure/container-apps/custom-domains-certificates) feature on the `twenty-front` container if you would like an easier domain name. #### Files ##### providers.tf ```hcl # providers.tf terraform { required_providers { azapi = { source = "Azure/azapi" } } } provider "azapi" { } provider "azurerm" { features {} } provider "azuread" { } provider "random" { } ``` ##### main.tf ```hcl # main.tf # Create a resource group resource "azurerm_resource_group" "main" { name = "twenty-crm-rg" location = "North Europe" } # Variables locals { app_env_name = "twenty" server_name = "twenty-server" server_tag = "latest" front_app_name = "twenty-front" front_tag = "latest" db_app_name = "twenty-postgres" db_tag = "latest" db_user = "twenty" db_password = "twenty" storage_mount_db_name = "twentydbstoragemount" storage_mount_server_name = "twentyserverstoragemount" cpu = 1.0 memory = "2Gi" } # Set up a Log Analytics workspace resource "azurerm_log_analytics_workspace" "main" { name = "${local.app_env_name}-law" location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name sku = "PerGB2018" retention_in_days = 30 } # Create a storage account resource "random_pet" "example" { length = 2 separator = "" } resource "azurerm_storage_account" "main" { name = "twentystorage${random_pet.example.id}" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location account_tier = "Standard" account_replication_type = "LRS" large_file_share_enabled = true } # Create db file storage resource "azurerm_storage_share" "db" { name = "twentydatabaseshare" storage_account_name = azurerm_storage_account.main.name quota = 50 enabled_protocol = "SMB" } # Create backend file storage resource "azurerm_storage_share" "server" { name = "twentyservershare" storage_account_name = azurerm_storage_account.main.name quota = 50 enabled_protocol = "SMB" } # Create a Container App Environment resource "azurerm_container_app_environment" "main" { name = "${local.app_env_name}-env" location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id } # Connect the db storage share to the container app environment resource "azurerm_container_app_environment_storage" "db" { name = local.storage_mount_db_name container_app_environment_id = azurerm_container_app_environment.main.id account_name = azurerm_storage_account.main.name share_name = azurerm_storage_share.db.name access_key = azurerm_storage_account.main.primary_access_key access_mode = "ReadWrite" } # Connect the server storage share to the container app environment resource "azurerm_container_app_environment_storage" "server" { name = local.storage_mount_server_name container_app_environment_id = azurerm_container_app_environment.main.id account_name = azurerm_storage_account.main.name share_name = azurerm_storage_share.server.name access_key = azurerm_storage_account.main.primary_access_key access_mode = "ReadWrite" } ``` ##### frontend.tf ```hcl # frontend.tf resource "azurerm_container_app" "twenty_front" { name = local.front_app_name container_app_environment_id = azurerm_container_app_environment.main.id resource_group_name = azurerm_resource_group.main.name revision_mode = "Single" depends_on = [azurerm_container_app.twenty_server] ingress { allow_insecure_connections = false external_enabled = true target_port = 3000 transport = "http" traffic_weight { percentage = 100 latest_revision = true } } template { min_replicas = 1 # things starts to fail when using more than 1 replica max_replicas = 1 container { name = "twenty-front" image = "docker.io/twentycrm/twenty-front:${local.front_tag}" cpu = local.cpu memory = local.memory env { name = "REACT_APP_SERVER_BASE_URL" value = "https://${azurerm_container_app.twenty_server.ingress[0].fqdn}" } } } } # Set CORS rules for frontend app using AzAPI resource "azapi_update_resource" "cors" { type = "Microsoft.App/containerApps@2023-05-01" resource_id = azurerm_container_app.twenty_front.id body = jsonencode({ properties = { configuration = { ingress = { corsPolicy = { allowedOrigins = ["*"] } } } } }) depends_on = [azurerm_container_app.twenty_front] } ``` ##### backend.tf ```hcl # backend.tf # Create three random UUIDs resource "random_uuid" "access_token_secret" {} resource "random_uuid" "login_token_secret" {} resource "random_uuid" "refresh_token_secret" {} resource "random_uuid" "file_token_secret" {} resource "azurerm_container_app" "twenty_server" { name = local.server_name container_app_environment_id = azurerm_container_app_environment.main.id resource_group_name = azurerm_resource_group.main.name revision_mode = "Single" depends_on = [azurerm_container_app.twenty_db, azurerm_container_app_environment_storage.server] ingress { allow_insecure_connections = false external_enabled = true target_port = 3000 transport = "http" traffic_weight { percentage = 100 latest_revision = true } } template { min_replicas = 1 max_replicas = 1 volume { name = "twenty-server-data" storage_type = "AzureFile" storage_name = local.storage_mount_server_name } container { name = local.server_name image = "docker.io/twentycrm/twenty-server:${local.server_tag}" cpu = local.cpu memory = local.memory volume_mounts { name = "twenty-server-data" path = "/app/packages/twenty-server/.local-storage" } # Environment variables env { name = "IS_SIGN_UP_DISABLED" value = false } env { name = "SIGN_IN_PREFILLED" value = false } env { name = "STORAGE_TYPE" value = "local" } env { name = "STORAGE_LOCAL_PATH" value = ".local-storage" } env { name = "PG_DATABASE_URL" value = "postgres://${local.db_user}:${local.db_password}@${local.db_app_name}:5432/default" } env { name = "FRONT_BASE_URL" value = "https://${local.front_app_name}" } env { name = "ACCESS_TOKEN_SECRET" value = random_uuid.access_token_secret.result } env { name = "LOGIN_TOKEN_SECRET" value = random_uuid.login_token_secret.result } env { name = "REFRESH_TOKEN_SECRET" value = random_uuid.refresh_token_secret.result } env { name = "FILE_TOKEN_SECRET" value = random_uuid.file_token_secret.result } } } } # Set CORS rules for server app using AzAPI resource "azapi_update_resource" "server_cors" { type = "Microsoft.App/containerApps@2023-05-01" resource_id = azurerm_container_app.twenty_server.id body = jsonencode({ properties = { configuration = { ingress = { corsPolicy = { allowedOrigins = ["*"] } } } } }) depends_on = [azurerm_container_app.twenty_server] } ``` ##### database.tf ```hcl # database.tf resource "azurerm_container_app" "twenty_db" { name = local.db_app_name container_app_environment_id = azurerm_container_app_environment.main.id resource_group_name = azurerm_resource_group.main.name revision_mode = "Single" depends_on = [azurerm_container_app_environment_storage.db] ingress { allow_insecure_connections = false external_enabled = false target_port = 5432 transport = "tcp" traffic_weight { percentage = 100 latest_revision = true } } template { min_replicas = 1 max_replicas = 1 container { name = local.db_app_name image = "docker.io/twentycrm/twenty-postgres:${local.db_tag}" cpu = local.cpu memory = local.memory volume_mounts { name = "twenty-db-data" path = "/var/lib/postgresql/data" } env { name = "POSTGRES_USER" value = "postgres" } env { name = "POSTGRES_PASSWORD" value = "postgres" } env { name = "POSTGRES_DB" value = "default" } } volume { name = "twenty-db-data" storage_type = "AzureFile" storage_name = local.storage_mount_db_name } } } ``` ## Others Please feel free to Open a PR to add more Cloud Provider options.