Azure Container Instance (ACI) — Standalone installation

Support DQE
Support DQE
  • Updated

Note: All elements described below are recommendations from DQE, based on deployment experience with different customers. The customer is responsible for integrating the solution into their own architecture. An architect familiar with the customer context and internal infrastructure should align DQE's recommendations with the client infrastructure.

1. Architecture

This document describes how to deploy the DQE One Standalone backend as an Azure Container Instance (ACI) container group. All containers in the group share the same network namespace and communicate through localhost. An Application Gateway exposes the application over HTTPS and routes traffic to the ACI through a private VNET.

Recommended architecture

Internet (HTTPS port 443)
         |
Application Gateway (public IP + WAF)
         |
    Private VNET subnet
         |
ACI Container Group (private IP)
         |
    NGINX sidecar (port 443)
         |
  DQE One Standalone (port 8000, internal)
         |
  Redis - RabbitMQ - PostgreSQL (internal)

Security measures

  • Application Gateway: terminates HTTPS and routes traffic to the ACI through the private VNET. Use the WAF to restrict inbound IP ranges.
  • VNET: isolates the ACI from direct public internet access. All traffic from the Application Gateway passes through a private subnet.
  • SSL certificate: managed at the Application Gateway level or at the NGINX level inside the ACI.
  • Secrets: use secureValue in the container group YAML for all sensitive values (passwords, licence keys, encryption keys).

Recommended sizing

Container CPU Memory
nginx 0.25 vCPU 0.5 GB
redis 0.5 vCPU 1.0 GB
rabbitmq 0.5 vCPU 1.0 GB
postgres 0.5 vCPU 1.0 GB
dqeone 1.0 vCPU 2.0 GB
Total 2.75 vCPU 5.5 GB

ACI container groups are limited to 4 vCPU and 16 GB of memory per group.

2. Installation

2.1. Azure CLI

Azure CLI must be installed on your local machine.

Linux / macOS

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Windows

Refer to the Microsoft documentation.

2.2. Create an Application Gateway

The Application Gateway exposes the ACI with a public IP address and DNS name. It also handles HTTPS termination and IP filtering via the WAF.

For full configuration details, refer to the Microsoft Application Gateway documentation.

Basic tab

Select your subscription, resource group, and region. Choose the WAF V2 tier to enable the Web Application Firewall.

WAF Policy

Create a new WAF policy from the WAF policy field. Use the WAF to define inbound IP ranges authorised to call the Application Gateway (for example, your corporate network or specific partner IP ranges).

VNET

Create a new Virtual Network (VNET) from the Virtual network field. This VNET connects the Application Gateway to the ACI. All traffic between the two passes through this private network.

Frontends

Create a new public IP address. This is the IP address exposed externally and used to route traffic to the ACI.

Backends

Create a new backend pool. Leave it empty for now — the private IP address of the ACI is added after the ACI is deployed (section 3.3).

Configuration — Routing rules

Create a routing rule with:

  • Listener: HTTPS on port 443, with your SSL certificate.
  • Backend target: the backend pool created above.
  • Backend setting: HTTP on port 80 (traffic between the Application Gateway and the ACI travels through the private VNET and does not require HTTPS internally).

Click Review + create to deploy the Application Gateway.

2.3. Add a VNET Subnet for the ACI

In the VNET created in section 2.2, create a new subnet dedicated to the ACI. Delegate this subnet to Microsoft.ContainerInstance/containerGroups.

Note the full subnet resource ID — it is required in the container group YAML:

/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/Microsoft.Network/virtualNetworks/VNET_NAME/subnets/SUBNET_NAME

2.4. Configure Azure Storage

ACI containers use Azure File Shares for persistent storage. Create a storage account, then create the following file shares:

File share Purpose
nginxconf NGINX configuration and SSL certificate files
redisdata Redis persistent data
rabbitmqdata RabbitMQ persistent data
postgresdata PostgreSQL data directory

Step 1 — Create the storage account:

az storage account create \
  --name MY_STORAGE_ACCOUNT \
  --resource-group MY_RESOURCE_GROUP \
  --location MY_LOCATION \
  --sku Standard_LRS

Step 2 — Retrieve the storage account key:

az storage account keys list \
  --account-name MY_STORAGE_ACCOUNT \
  --resource-group MY_RESOURCE_GROUP \
  --query "[0].value" -o tsv

Step 3 — Create the file shares:

az storage share create --name nginxconf --account-name MY_STORAGE_ACCOUNT
az storage share create --name redisdata --account-name MY_STORAGE_ACCOUNT
az storage share create --name rabbitmqdata --account-name MY_STORAGE_ACCOUNT
az storage share create --name postgresdata --account-name MY_STORAGE_ACCOUNT

Note on PostgreSQL storage: Azure File Shares use the SMB protocol, which may not support all POSIX filesystem operations required by PostgreSQL. If the postgres container fails to start, refer to section 5 for the recommended alternative using Azure Database for PostgreSQL.

2.5. Create a Log Analytics Workspace

A Log Analytics workspace centralises container logs and enables monitoring through Azure Monitor. Logs are stored for 30 days by default.

Step 1 — Create the workspace:

az monitor log-analytics workspace create \
  --resource-group MY_RESOURCE_GROUP \
  --workspace-name dqe-standalone-logs \
  --location MY_LOCATION

Step 2 — Retrieve the Workspace ID:

az monitor log-analytics workspace show \
  --resource-group MY_RESOURCE_GROUP \
  --workspace-name dqe-standalone-logs \
  --query customerId -o tsv

Step 3 — Retrieve the Workspace Primary Key:

az monitor log-analytics workspace get-shared-keys \
  --resource-group MY_RESOURCE_GROUP \
  --workspace-name dqe-standalone-logs \
  --query primarySharedKey -o tsv

Keep the Workspace ID and Primary Key — they are required in the container group YAML (section 2.7).

2.6. Configure NGINX

NGINX acts as a reverse proxy inside the ACI container group, forwarding incoming requests to the dqeone container on port 8000. Because all containers in an ACI group share the same network namespace, the proxy target uses localhost.

Create a file named default.conf with the following content:

server {
    listen 80;

    location / {
        proxy_pass         http://localhost:8000;
        proxy_read_timeout 300s;
    }
}

If HTTPS is handled directly at the ACI level (without Application Gateway SSL offloading), add a second server block for port 443 and upload the SSL certificate and key files to the nginxconf file share under an ssl/ subdirectory.

Upload default.conf to the nginxconf file share:

az storage file upload \
  --account-name MY_STORAGE_ACCOUNT \
  --share-name nginxconf \
  --source ./default.conf \
  --path default.conf

2.7. Container group YAML

Create a file named container-group.yaml. Replace all placeholders before deploying. The table below lists all required values.

Placeholder Description
MY_LOCATION Azure region, for example westeurope
DQE_REGISTRY_LOGIN Registry username provided by DQE
DQE_REGISTRY_PASSWORD Registry password provided by DQE
MY_STORAGE_ACCOUNT Storage account name created in section 2.4
MY_STORAGE_ACCOUNT_KEY Storage account key retrieved in section 2.4 Step 2
LOG_ANALYTICS_WORKSPACE_ID Workspace ID retrieved in section 2.5 Step 2
LOG_ANALYTICS_WORKSPACE_KEY Primary Key retrieved in section 2.5 Step 3
SUBNET_RESOURCE_ID Full subnet resource ID noted in section 2.3
name: dqe-standalone
apiVersion: '2021-10-01'
location: MY_LOCATION
tags: {"docker-compose-application": "docker-compose-application"}

properties:
  containers:

    - name: nginx
      properties:
        image: nginx:latest
        ports:
          - protocol: TCP
            port: 80
        resources:
          requests:
            memoryInGB: 0.5
            cpu: 0.25
        volumeMounts:
          - name: nginxconf
            mountPath: /etc/nginx/conf.d

    - name: redis
      properties:
        image: dqeone.azurecr.io/dqe-one-redis:v1.0
        resources:
          requests:
            memoryInGB: 1.0
            cpu: 0.5
        volumeMounts:
          - name: redisdata
            mountPath: /data

    - name: rabbitmq
      properties:
        image: dqeone.azurecr.io/dqe-one-rabbitmq:v1.0
        ports:
          - protocol: TCP
            port: 5672
        resources:
          requests:
            memoryInGB: 1.0
            cpu: 0.5
        environmentVariables:
          - name: RABBITMQ_DEFAULT_PASS
            value: guest
          - name: RABBITMQ_DEFAULT_USER
            value: guest
          - name: RABBITMQ_DEFAULT_VHOST
            value: admin
        volumeMounts:
          - name: rabbitmqdata
            mountPath: /var/lib/rabbitmq
            readOnly: false

    - name: postgres
      properties:
        image: dqeone.azurecr.io/dqe-one-postgres:v1.0
        resources:
          requests:
            memoryInGB: 1.0
            cpu: 0.5
        environmentVariables:
          - name: POSTGRES_USER
            value: dqeone
          - name: POSTGRES_PASSWORD
            secureValue: DATABASE_PASSWORD
          - name: POSTGRES_DB
            value: dqeone
        volumeMounts:
          - name: postgresdata
            mountPath: /var/lib/postgresql/data

    - name: dqeone
      properties:
        image: dqeone.azurecr.io/standalone:v1.4.0
        command:
          - "bash"
          - "./entrypoint.sh"
        ports:
          - protocol: TCP
            port: 8000
        resources:
          requests:
            memoryInGB: 2.0
            cpu: 1.0
        environmentVariables:
          - name: SFAPIVERSION
            value: v65.0
          - name: CREATE_SUPERUSER
            value: "true"
          - name: RUN_COLLECTSTATIC
            value: "false"
          - name: DQE_ONE_SERVER_ADMIN_USER
            value: ADMIN_USER
          - name: DQE_ONE_SERVER_ADMIN_PASSWORD
            secureValue: ADMIN_PASSWORD
          - name: DQE_CLIENT_LICENCE
            value: CLIENT_LICENCE
          - name: WEBSITE_HOSTNAME
            value: https://YOUR_DNS_NAME
          - name: SECRET_ENCRYPTION_KEY
            secureValue: SECRET_ENCRYPTION_KEY_VALUE
          - name: WAIT_HOSTS
            value: "localhost:6379"
          - name: WAIT_HOSTS_TIMEOUT
            value: "300"
          - name: WAIT_SLEEP_INTERVAL
            value: "5"
          - name: WAIT_HOST_CONNECT_TIMEOUT
            value: "30"
          - name: REDIS_URL
            value: redis://localhost:6379
          - name: PORT
            value: "8000"
          - name: DEBUG
            value: "false"
          - name: DB_USER
            value: dqeone
          - name: DB_PASSWORD
            secureValue: DATABASE_PASSWORD
          - name: DB_NAME
            value: dqeone
          - name: DB_HOST
            value: localhost
          - name: DB_VOLUME_PATH
            value: ./db/
          - name: DB_MAX_CAPACITY
            value: "8000000000"
          - name: AUTHORIZED_SFTP_HOSTS
            value: AUTHORIZED_SFTP_HOSTS

  imageRegistryCredentials:
    - server: dqeone.azurecr.io
      username: DQE_REGISTRY_LOGIN
      password: DQE_REGISTRY_PASSWORD

  diagnostics:
    logAnalytics:
      workspaceId: LOG_ANALYTICS_WORKSPACE_ID
      workspaceKey: LOG_ANALYTICS_WORKSPACE_KEY

  restartPolicy: Always

  ipAddress:
    ports:
      - protocol: TCP
        port: 80
    type: Private

  osType: Linux

  volumes:
    - name: nginxconf
      azureFile:
        shareName: nginxconf
        readOnly: false
        storageAccountName: MY_STORAGE_ACCOUNT
        storageAccountKey: MY_STORAGE_ACCOUNT_KEY
    - name: redisdata
      azureFile:
        shareName: redisdata
        readOnly: false
        storageAccountName: MY_STORAGE_ACCOUNT
        storageAccountKey: MY_STORAGE_ACCOUNT_KEY
    - name: rabbitmqdata
      azureFile:
        shareName: rabbitmqdata
        readOnly: false
        storageAccountName: MY_STORAGE_ACCOUNT
        storageAccountKey: MY_STORAGE_ACCOUNT_KEY
    - name: postgresdata
      azureFile:
        shareName: postgresdata
        readOnly: false
        storageAccountName: MY_STORAGE_ACCOUNT
        storageAccountKey: MY_STORAGE_ACCOUNT_KEY

  subnetIds:
    - id: SUBNET_RESOURCE_ID

Important: use the image versions provided by DQE. Do not replace them with the latest tag.

Key ACI networking note: all containers share the same network namespace. Inter-container communication uses localhost — this is why REDIS_URL is redis://localhost:6379 and DB_HOST is localhost, unlike a Docker Compose setup where service names are used.

2.8. Environment variables

Variable Example value Description
SFAPIVERSION v65.0 Salesforce API version used by the application.
CREATE_SUPERUSER true Creates the initial administrator account during the first startup.
RUN_COLLECTSTATIC false Executes the Django collectstatic command during startup. Set to false unless explicitly required.
DQE_ONE_SERVER_ADMIN_USER Username of the initial administrator account.
DQE_ONE_SERVER_ADMIN_PASSWORD Password of the initial administrator account. Use secureValue.
DQE_CLIENT_LICENCE Customer licence key provided by DQE.
WEBSITE_HOSTNAME https://myapp.example.com Public HTTPS URL. Must match the DNS name pointing to the Application Gateway.
SECRET_ENCRYPTION_KEY Encryption key for sensitive data. Generate once, never change after deployment. Use secureValue.
WAIT_HOSTS localhost:6379 Service to wait for before starting. Uses localhost in ACI.
WAIT_HOSTS_TIMEOUT 300 Maximum wait time in seconds for dependent services.
WAIT_SLEEP_INTERVAL 5 Delay in seconds between availability checks.
WAIT_HOST_CONNECT_TIMEOUT 30 Timeout in seconds for each connection attempt.
REDIS_URL redis://localhost:6379 Redis connection URL. Uses localhost in ACI.
PORT 8000 Internal listening port of the application.
DEBUG false Debug mode. Must be false in production.
DB_USER dqeone PostgreSQL username.
DB_PASSWORD PostgreSQL password. Must match POSTGRES_PASSWORD in the postgres container. Use secureValue.
DB_NAME dqeone PostgreSQL database name.
DB_HOST localhost PostgreSQL hostname. Uses localhost in ACI. Set to managed server hostname if using Azure Database for PostgreSQL.
DB_VOLUME_PATH ./db/ Path for database-related storage.
DB_MAX_CAPACITY 8000000000 Maximum database capacity in bytes.
AUTHORIZED_SFTP_HOSTS depot-1.dqe-software.net Comma-separated list of authorised SFTP hosts.

Important: the SECRET_ENCRYPTION_KEY must be generated once and kept for the lifetime of the deployment. To generate a compatible key:

python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

3. Launcher

3.1. Log in to Azure

az login

3.2. Deploy the container group

az container create -g MY_RESOURCE_GROUP -f container-group.yaml

After the operation is complete, retrieve the private IP address assigned to the container group:

az container show \
  --resource-group MY_RESOURCE_GROUP \
  --name dqe-standalone \
  --query "ipAddress.ip" -o tsv

This private IP address is reachable only from within the Azure VNET.

3.3. Set up the Application Gateway backend pool

After the ACI is deployed, go to Azure Portal > Application Gateway > Backend pools > your backend pool > Edit.

Add the private IP address of the ACI retrieved in step 3.2. Click Save.

3.4. Set up the Application Gateway health probe

The health probe allows the Application Gateway to verify that the application is running. Go to Azure Portal > Application Gateway > Health probes > Add.

Configure the probe to call the application root path over HTTP. Click Test. If the status shows Healthy, the ACI is properly configured and the Application Gateway can route traffic to it.

Click Add to save the health probe.

3.5. Set up DNS

Contact your DNS administrators to create a DNS record pointing to the public IP address of the Application Gateway:

Record type Name Value
A standalone.yourdomain.com Public IP of the Application Gateway

3.6. Verify the installation

Check that all containers are running:

az container show \
  --resource-group MY_RESOURCE_GROUP \
  --name dqe-standalone \
  --query "containers[].{Name:name, State:instanceView.currentState.state}" \
  -o table

Expected output:

Name        State
--------    -------
nginx       Running
redis       Running
rabbitmq    Running
postgres    Running
dqeone      Running

Note: ACI starts all containers simultaneously. The WAIT_HOSTS mechanism handles startup order by retrying the connection. A delay of 1–2 minutes before the application is fully operational is expected on first start.

Once all containers are running and DNS has propagated, navigate to https://standalone.yourdomain.com.

4. IP Addresses to Authorise

Once the application is running, configure the WAF or upstream firewall to authorise the following inbound IP ranges:

  • DQE Software Office Server: contact DQE Software support to obtain the IP address to authorise.
  • DQE Deduplication Service: contact DQE Software support to obtain the IP address to authorise.
  • DQE Quality Service: contact DQE Software support to obtain the IP address to authorise.

Action required: provide DQE Software with the outbound public IP address used by your Azure infrastructure (NAT Gateway, Azure Firewall, or equivalent) so that it can be authorised on DQE services.

5. Troubleshooting

View container logs

Container logs are available in Azure Monitor under the ContainerInstanceLog_CL table. You can also retrieve them directly via CLI:

az container logs \
  --resource-group MY_RESOURCE_GROUP \
  --name dqe-standalone \
  --container-name CONTAINER_NAME

Unauthorised while pulling images

Verify that:

  • the imageRegistryCredentials section contains the correct login and password provided by DQE;
  • all images reference the DQE production registry dqeone.azurecr.io;
  • the image versions match those provided by DQE.

PostgreSQL container fails to start

Azure File Shares use the SMB protocol, which may not support the POSIX filesystem operations required by PostgreSQL. If the postgres container repeatedly restarts with permission errors, use Azure Database for PostgreSQL — Flexible Server instead:

  • Remove the postgres container and the postgresdata volume from the YAML.
  • Set DB_HOST to the managed server hostname (for example: myserver.postgres.database.azure.com).
  • Update DB_USER, DB_PASSWORD, and DB_NAME to match the managed server credentials.

Application Gateway health probe fails

Verify that:

  • all ACI containers show state Running;
  • the backend pool contains the correct private IP address of the ACI;
  • the subnet is properly delegated to Microsoft.ContainerInstance/containerGroups;
  • the NSG attached to the subnet allows inbound traffic on port 80 from the Application Gateway subnet.

Was this article helpful?

0 out of 0 found this helpful