Azure Container Instance (ACI) — DQE One 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 80, internal)
         |
  DQE One Standalone (port 8000, internal)
         |
       Redis (internal)
         |
Azure Database for PostgreSQL (private VNET)

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
dqeone 1.0 vCPU 2.0 GB
Total 1.75 vCPU 3.5 GB

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

PostgreSQL is hosted on Azure Database for PostgreSQL (outside the ACI container group). See section 2.5 for setup.

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.

VNET
Create a new Virtual Network (VNET) from the Virtual network field. This VNET connects the Application Gateway to the ACI.

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.

az network vnet subnet create \
  --resource-group MY_RESOURCE_GROUP \
  --vnet-name VNET_NAME \
  --name aci-subnet \
  --address-prefixes 10.0.1.0/24 \
  --delegations 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

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

2.5. Create Azure Database for PostgreSQL

PostgreSQL is hosted as a managed Azure service outside the ACI container group. Azure File Shares (SMB) do not support the POSIX filesystem operations required by PostgreSQL and will cause a CrashLoopBackOff.

Step 1 — Create a dedicated subnet for PostgreSQL

In the VNET created in section 2.2, add a new subnet (e.g. postgres-subnet). Delegate it to Microsoft.DBforPostgreSQL/flexibleServers. This subnet cannot be shared with the ACI subnet.

az network vnet subnet create \
  --resource-group MY_RESOURCE_GROUP \
  --vnet-name VNET_NAME \
  --name postgres-subnet \
  --address-prefixes 10.0.2.0/24 \
  --delegations Microsoft.DBforPostgreSQL/flexibleServers

Step 2 — Create the server

In the Azure portal, create an Azure Database for PostgreSQL resource:

  • Networking: select Private access (VNet Integration).
  • Virtual network: select the same VNet as the ACI. The ACI and the server must share the same VNet.
  • Subnet: select the subnet created in Step 1.
  • Admin username / password: set credentials. These values are used in DB_USER and DB_PASSWORD in the container group YAML.
az postgres flexible-server create \
  --resource-group MY_RESOURCE_GROUP \
  --name SERVERNAME \
  --location MY_LOCATION \
  --vnet VNET_NAME \
  --subnet postgres-subnet \
  --admin-user DB_ADMIN_USER \
  --admin-password "DB_ADMIN_PASSWORD" \
  --sku-name Standard_B1ms \
  --tier Burstable \
  --version 16

Step 3 — Create the application database

In the Azure portal, go to the Azure Database for PostgreSQL resource → Databases+ Add, enter dqeone as the name, and save.

az postgres flexible-server db create \
  --resource-group MY_RESOURCE_GROUP \
  --server-name SERVERNAME \
  --database-name dqeone

2.6. 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.8).

2.7. 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.8. Container Group YAML

Create a file named container-group.yaml. Replace all placeholders before deploying.

Placeholder Description
MY_LOCATION Azure region, for example francecentral
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.6 Step 2
LOG_ANALYTICS_WORKSPACE_KEY Primary Key retrieved in section 2.6 Step 3
SUBNET_RESOURCE_ID Full ACI subnet resource ID noted in section 2.3
SERVERNAME PostgreSQL server name created in section 2.5
DB_ADMIN_USER PostgreSQL server admin username
DB_ADMIN_PASSWORD PostgreSQL server admin password
DQE_ADMIN_USER DQE One application admin username
DQE_ADMIN_PASSWORD DQE One application admin password
name: dqe-standalone
apiVersion: '2021-10-01'
location: MY_LOCATION
tags: {"docker-compose-application": "docker-compose-application"}

properties:
  containers:

    - name: nginx
      properties:
        image: dqeone.azurecr.io/dqe-one-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: 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: DQE_ADMIN_USER
          - name: DQE_ONE_SERVER_ADMIN_PASSWORD
            secureValue: DQE_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: DB_ADMIN_USER
          - name: DB_PASSWORD
            secureValue: DB_ADMIN_PASSWORD
          - name: DB_NAME
            value: dqeone
          - name: DB_HOST
            value: SERVERNAME.postgres.database.azure.com
          - name: DB_PORT
            value: "5432"
          - name: DB_VOLUME_PATH
            value: ./db/
          - name: DB_MAX_CAPACITY
            value: "8000000000"
          - name: AUTHORIZED_SFTP_HOSTS
            value: AUTHORIZED_SFTP_HOSTS
          # Only required if WEBSITE_HOSTNAME does not match the URL used to access the app:
          # - name: CSRF_TRUSTED_ORIGINS
          #   value: https://YOUR_IP_OR_ALTERNATE_URL

  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

  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 in the group share the same network namespace. Inter-container communication uses localhost — this is why REDIS_URL is redis://localhost:6379. PostgreSQL runs outside the container group as a managed Azure service, so DB_HOST is the Azure Database for PostgreSQL FQDN.

2.9. 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 admin username set during Postgres Server creation.
DB_PASSWORD PostgreSQL admin password. Use secureValue.
DB_NAME dqeone PostgreSQL database name. Required — the application will not start without it.
DB_HOST myserver.postgres.database.azure.com PostgreSQL server FQDN.
DB_PORT 5432 PostgreSQL port.
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.
CSRF_TRUSTED_ORIGINS https://myapp.example.com Trusted origins for Django CSRF. Only required if accessing the app via a URL that differs from WEBSITE_HOSTNAME.

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
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 Setup

Follow section 2.5 for the complete setup procedure. Key points:

  • The server must be in the same VNet as the ACI — different VNets require peering and DNS zone configuration.
  • Azure Database for PostgreSQL requires a dedicated subnet delegated to Microsoft.DBforPostgreSQL/flexibleServers.
  • After server creation, create the dqeone database from the Azure portal: Azure Database for PostgreSQL → Databases → + Add.
  • All DB_* environment variables must be set in the container group YAML before the first deployment — see section 2.8.

CSRF Verification Failed (403)

Django validates that inbound requests originate from a trusted origin matching WEBSITE_HOSTNAME. A 403 CSRF error occurs when accessing the application via a URL that differs from the configured hostname.

Permanent fix: configure DNS so that the hostname in WEBSITE_HOSTNAME resolves to the Application Gateway public IP.

Temporary workaround (testing without DNS): set WEBSITE_HOSTNAME to the IP address and add CSRF_TRUSTED_ORIGINS:

- name: WEBSITE_HOSTNAME
  value: https://YOUR_GATEWAY_IP
- name: CSRF_TRUSTED_ORIGINS
  value: https://YOUR_GATEWAY_IP

Once DNS is configured, revert both values to the proper domain name.

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