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
secureValuein 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
imageRegistryCredentialssection 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
postgrescontainer and thepostgresdatavolume from the YAML. - Set
DB_HOSTto the managed server hostname (for example:myserver.postgres.database.azure.com). - Update
DB_USER,DB_PASSWORD, andDB_NAMEto 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.