<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Khanh Nguyen</title>
    <description>The latest articles on DEV Community by Khanh Nguyen (@khanh_nguyen_04b1205f5a91).</description>
    <link>https://dev.to/khanh_nguyen_04b1205f5a91</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1559613%2F05e1dbdc-d265-4000-bdaf-e2cda4bacaea.jpg</url>
      <title>DEV Community: Khanh Nguyen</title>
      <link>https://dev.to/khanh_nguyen_04b1205f5a91</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khanh_nguyen_04b1205f5a91"/>
    <language>en</language>
    <item>
      <title>Deploy Grafana to Azure with Azure CLI : From Local Docker to Durable Dashboards</title>
      <dc:creator>Khanh Nguyen</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:04:12 +0000</pubDate>
      <link>https://dev.to/khanh_nguyen_04b1205f5a91/deploy-grafana-to-azure-with-azure-cli-from-local-docker-to-durable-dashboards-5f0l</link>
      <guid>https://dev.to/khanh_nguyen_04b1205f5a91/deploy-grafana-to-azure-with-azure-cli-from-local-docker-to-durable-dashboards-5f0l</guid>
      <description>&lt;p&gt;Grafana turns data from sources such as PostgreSQL, Prometheus, and Azure Monitor into shared dashboards, charts, and alerts. While it queries that operational data, it also needs to persist its own dashboards, users, alert rules, and data-source configuration.&lt;/p&gt;

&lt;p&gt;I usually use IaC for shared Azure environments. For this deployment, I used Azure CLI first to understand and validate each dependency directly: private networking, PostgreSQL persistence, ACR image pulls, managed identity, and Container Apps secrets.&lt;/p&gt;

&lt;p&gt;This guide deploys Grafana on Azure Container Apps with Azure Database for PostgreSQL Flexible Server and a pinned image in ACR. By the end, you will have managed HTTPS ingress, private database connectivity, persistent Grafana state, and no registry password in the deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a production-minded lab: no &lt;code&gt;latest&lt;/code&gt; tag, anonymous access disabled, and no database password baked into the image. After validating the design, capture it in Bicep, Terraform, or your preferred IaC tool for repeatable deployments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The architecture: separate what changes from what must persist
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuw0ecvx95rksvq6ywbeb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuw0ecvx95rksvq6ywbeb.png" alt="pic_1" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service responsibilities are deliberately separate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Azure Container Apps&lt;/td&gt;
&lt;td&gt;Runs Grafana, provides HTTPS ingress, and manages revisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Database for PostgreSQL&lt;/td&gt;
&lt;td&gt;Stores Grafana's users, dashboards, alerting configuration, and settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACR&lt;/td&gt;
&lt;td&gt;Provides a private, deployable copy of the Grafana image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-assigned managed identity&lt;/td&gt;
&lt;td&gt;Lets Container Apps pull from ACR without registry credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Grafana's &lt;strong&gt;internal database&lt;/strong&gt; is different from the reporting database that dashboards query. This distinction is easy to miss: the internal database preserves Grafana itself, while data sources provide the metrics and business data displayed in a dashboard. Give Grafana a dedicated database and role; give dashboards a separate, read-only data-source account where possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start: prepare the Azure CLI session
&lt;/h2&gt;

&lt;p&gt;You need an Azure subscription, Docker, and the Azure CLI. The commands use Bash or zsh; use a shell such as Azure Cloud Shell, macOS Terminal, or WSL.&lt;/p&gt;

&lt;p&gt;Sign in and install the Container Apps extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login
az extension add &lt;span class="nt"&gt;--name&lt;/span&gt; containerapp &lt;span class="nt"&gt;--upgrade&lt;/span&gt;

az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.App
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.OperationalInsights
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerService
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerRegistry
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.DBforPostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set names that are unique where Azure requires them. Keep secrets out of your shell history and Git repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RESOURCE_GROUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rg-grafana-dev
&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;canadacentral

&lt;span class="nv"&gt;VNET_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vnet-grafana-dev
&lt;span class="nv"&gt;ACA_SUBNET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;snet-containerapps
&lt;span class="nv"&gt;POSTGRES_SUBNET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;snet-postgres
&lt;span class="nv"&gt;PRIVATE_DNS_ZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grafana.private.postgres.database.azure.com

&lt;span class="nv"&gt;POSTGRES_SERVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pg-grafana-dev
&lt;span class="nv"&gt;POSTGRES_ADMIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pgadmin
&lt;span class="nv"&gt;GRAFANA_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grafana
&lt;span class="nv"&gt;GRAFANA_DB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grafana_app

&lt;span class="nv"&gt;ACR_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;globally-unique-acr-name&amp;gt;
&lt;span class="nv"&gt;IDENTITY_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;id-grafana-acr-pull
&lt;span class="nv"&gt;ACA_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cae-grafana-dev
&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grafana

&lt;span class="c"&gt;# Pin the version that you tested. Do not deploy :latest.&lt;/span&gt;
&lt;span class="nv"&gt;GRAFANA_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;13.1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collect secret values interactively. Replace these with Azure Key Vault references for a shared or production environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-rsp&lt;/span&gt; &lt;span class="s2"&gt;"PostgreSQL admin password: "&lt;/span&gt; POSTGRES_ADMIN_PASSWORD&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
read&lt;/span&gt; &lt;span class="nt"&gt;-rsp&lt;/span&gt; &lt;span class="s2"&gt;"Grafana database password: "&lt;/span&gt; GRAFANA_DB_PASSWORD&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
read&lt;/span&gt; &lt;span class="nt"&gt;-rsp&lt;/span&gt; &lt;span class="s2"&gt;"Grafana admin password: "&lt;/span&gt; GRAFANA_ADMIN_PASSWORD&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 48&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The server name, ACR name, and PostgreSQL administrator shown above are examples. Do not reuse the sample passwords or enable anonymous access from a local experiment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Optional: validate Grafana locally
&lt;/h2&gt;

&lt;p&gt;This optional check proves the image and database configuration before Azure resources are created. If PostgreSQL runs on your host, Docker Desktop exposes it as &lt;code&gt;host.docker.internal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a local-only &lt;code&gt;.env&lt;/code&gt; file. Never commit it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GF_DATABASE_TYPE=postgres
GF_DATABASE_HOST=host.docker.internal:5432
GF_DATABASE_NAME=grafana
GF_DATABASE_USER=grafana_app
GF_DATABASE_PASSWORD=replace-me
GF_DATABASE_SSL_MODE=disable
GF_SECURITY_ADMIN_PASSWORD=replace-me
GF_SECURITY_SECRET_KEY=replace-with-a-long-random-value
GF_AUTH_ANONYMOUS_ENABLED=false
GF_PANELS_DISABLE_SANITIZE_HTML=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the pinned image and check its health endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull &lt;span class="s2"&gt;"grafana/grafana:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; grafana &lt;span class="nt"&gt;--publish&lt;/span&gt; 3000:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="s2"&gt;"grafana/grafana:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;--fail&lt;/span&gt; http://localhost:3000/api/health
docker logs grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux Docker Engine, add &lt;code&gt;--add-host=host.docker.internal:host-gateway&lt;/code&gt; if needed. This hostname is local-only; Azure uses the PostgreSQL server FQDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give Grafana durable, private state
&lt;/h2&gt;

&lt;p&gt;Create the resource group, VNet, and two dedicated subnets. The Container Apps environment requires its own subnet. PostgreSQL Flexible Server requires a separate subnet delegated to &lt;code&gt;Microsoft.DBforPostgreSQL/flexibleServers&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

az network vnet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--address-prefixes&lt;/span&gt; 10.0.0.0/16

az network vnet subnet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACA_SUBNET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--address-prefixes&lt;/span&gt; 10.0.0.0/21 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delegations&lt;/span&gt; Microsoft.App/environments

az network vnet subnet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_SUBNET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--address-prefixes&lt;/span&gt; 10.0.8.0/24 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delegations&lt;/span&gt; Microsoft.DBforPostgreSQL/flexibleServers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create and link the private DNS zone. The link allows workloads in the VNet to resolve the database server to its private address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az network private-dns zone create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIVATE_DNS_ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

az network private-dns &lt;span class="nb"&gt;link &lt;/span&gt;vnet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--zone-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIVATE_DNS_ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; grafana-vnet-link &lt;span class="nt"&gt;--virtual-network&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registration-enabled&lt;/span&gt; &lt;span class="nb"&gt;false

&lt;/span&gt;&lt;span class="nv"&gt;PRIVATE_DNS_ZONE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az network private-dns zone show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIVATE_DNS_ZONE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create PostgreSQL and the Grafana database. PostgreSQL 18 is used here because it is currently supported by Flexible Server; choose a supported version approved by your organization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az postgres flexible-server create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_SERVER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-user&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_ADMIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--admin-password&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 18 &lt;span class="nt"&gt;--tier&lt;/span&gt; Burstable &lt;span class="nt"&gt;--sku-name&lt;/span&gt; Standard_B1ms &lt;span class="nt"&gt;--storage-size&lt;/span&gt; 32 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vnet&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--subnet&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_SUBNET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-dns-zone&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIVATE_DNS_ZONE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

az postgres flexible-server db create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--server-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_SERVER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;POSTGRES_FQDN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az postgres flexible-server show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_SERVER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; fullyQualifiedDomainName &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a least-privileged Grafana role
&lt;/h3&gt;

&lt;p&gt;For a private database, run the following from a trusted machine that has network access to the VNet, such as a jump host. It creates an application role and makes it owner of the Grafana database, which lets Grafana run its own schema migrations without using the server administrator at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"host=&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_FQDN&lt;/span&gt;&lt;span class="s2"&gt; port=5432 dbname=postgres user=&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_ADMIN&lt;/span&gt;&lt;span class="s2"&gt; sslmode=require"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;db_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;db_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_DB_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;grafana_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_DB_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'db_user', :'grafana_password') &lt;/span&gt;&lt;span class="se"&gt;\g&lt;/span&gt;&lt;span class="sh"&gt;exec
SELECT format('ALTER DATABASE %I OWNER TO %I', :'db_name', :'db_user') &lt;/span&gt;&lt;span class="se"&gt;\g&lt;/span&gt;&lt;span class="sh"&gt;exec
&lt;/span&gt;&lt;span class="no"&gt;SQL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The FQDN is read from the provisioned server instead of assumed. In a production environment, manage this database setup through a reviewed migration or infrastructure workflow instead of typing credentials into an interactive terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Prepare a private image and passwordless pull access
&lt;/h2&gt;

&lt;p&gt;ACR can import the public image directly, so your deployment does not depend on Docker Hub at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az acr create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; Basic &lt;span class="nt"&gt;--role-assignment-mode&lt;/span&gt; rbac

&lt;span class="c"&gt;# Required for managed-identity pulls by Azure Container Apps.&lt;/span&gt;
az acr config authentication-as-arm update &lt;span class="nt"&gt;--registry&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--status&lt;/span&gt; enabled

&lt;span class="nv"&gt;ACR_LOGIN_SERVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az acr show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; loginServer &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az acr import &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="s2"&gt;"docker.io/grafana/grafana:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; &lt;span class="s2"&gt;"grafana:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning &lt;code&gt;13.1.3&lt;/code&gt; avoids the ambiguity of &lt;code&gt;latest&lt;/code&gt;. Tags can still be moved, so for an immutable production release, record and deploy the imported image digest. When you upgrade, test the new version, import it, and deploy it as a new Container Apps revision.&lt;/p&gt;

&lt;p&gt;Create a user-assigned managed identity and grant it pull-only access to this registry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az identity create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDENTITY_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;IDENTITY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az identity show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDENTITY_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;IDENTITY_PRINCIPAL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az identity show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDENTITY_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; principalId &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ACR_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az acr show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-object-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDENTITY_PRINCIPAL_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-principal-type&lt;/span&gt; ServicePrincipal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; AcrPull &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your registry uses the newer RBAC + ABAC repository-permission mode, use &lt;strong&gt;Container Registry Repository Reader&lt;/strong&gt; instead of &lt;code&gt;AcrPull&lt;/code&gt;, ideally scoped to the &lt;code&gt;grafana&lt;/code&gt; repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Run Grafana with secrets outside the image
&lt;/h2&gt;

&lt;p&gt;Retrieve the Container Apps subnet ID, then attach it when you create the environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ACA_SUBNET_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az network vnet subnet show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACA_SUBNET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az containerapp &lt;span class="nb"&gt;env &lt;/span&gt;create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACA_ENV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--infrastructure-subnet-resource-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACA_SUBNET_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the app with external HTTPS ingress. Secrets are stored by Container Apps and referenced with &lt;code&gt;secretref:&lt;/code&gt;. They are not image labels, plain environment-variable values in source control, or ACR credentials. This lab passes secret values through the CLI; use Key Vault references for production to avoid exposing secret values to the local process environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--environment&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACA_ENV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACR_LOGIN_SERVER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/grafana:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-assigned&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDENTITY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registry-server&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ACR_LOGIN_SERVER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--registry-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDENTITY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ingress&lt;/span&gt; external &lt;span class="nt"&gt;--target-port&lt;/span&gt; 3000 &lt;span class="nt"&gt;--transport&lt;/span&gt; auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 0.5 &lt;span class="nt"&gt;--memory&lt;/span&gt; 1Gi &lt;span class="nt"&gt;--min-replicas&lt;/span&gt; 1 &lt;span class="nt"&gt;--max-replicas&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secrets&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    grafana-db-password&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_DB_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    grafana-admin-password&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    grafana-secret-key&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAFANA_SECRET_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-vars&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_DATABASE_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"GF_DATABASE_HOST=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_FQDN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:5432"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"GF_DATABASE_NAME=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_DB&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"GF_DATABASE_USER=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_DB_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_DATABASE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secretref:grafana-db-password &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_DATABASE_SSL_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;require &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_SECURITY_ADMIN_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_SECURITY_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secretref:grafana-admin-password &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_SECURITY_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secretref:grafana-secret-key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_AUTH_ANONYMOUS_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;GF_PANELS_DISABLE_SANITIZE_HTML&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app is intentionally fixed at one replica. Grafana's application state lives in PostgreSQL, but a multi-replica Grafana design still needs deliberate consideration for plugins, sessions, provisioning, alerting, and load balancing. Start with one replica; scale only after validating those behaviours for your Grafana version and plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Confirm the deployment and persistence
&lt;/h2&gt;

&lt;p&gt;Retrieve the endpoint and open it in a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GRAFANA_FQDN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az containerapp show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; properties.configuration.ingress.fqdn &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_FQDN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign in with &lt;code&gt;admin&lt;/code&gt; and the password supplied as &lt;code&gt;GRAFANA_ADMIN_PASSWORD&lt;/code&gt; on Grafana's first start. Grafana applies &lt;code&gt;GF_SECURITY_ADMIN_PASSWORD&lt;/code&gt; only when it creates the initial admin user; later changes require Grafana's supported password-management path. If the revision does not become healthy, tail the container logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp logs show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; console &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most common failures are straightforward:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;First check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image pull fails&lt;/td&gt;
&lt;td&gt;Identity role has not propagated or has the wrong ACR role&lt;/td&gt;
&lt;td&gt;Verify the role assignment and wait briefly before retrying&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no such host&lt;/code&gt; / DB connection fails&lt;/td&gt;
&lt;td&gt;Private DNS zone is not linked or the FQDN is wrong&lt;/td&gt;
&lt;td&gt;Confirm the zone link and use the server's Azure FQDN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS or authentication failure&lt;/td&gt;
&lt;td&gt;Database SSL or credentials are incorrect&lt;/td&gt;
&lt;td&gt;Keep &lt;code&gt;GF_DATABASE_SSL_MODE=require&lt;/code&gt; and rotate/reapply the secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana starts but settings disappear&lt;/td&gt;
&lt;td&gt;Grafana is using SQLite or the wrong database&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;GF_DATABASE_*&lt;/code&gt; variables and startup logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a quick persistence check, create a throwaway dashboard, trigger a new Container Apps revision with the same image, and confirm that the dashboard remains after the revision becomes active. If it disappears, stop and fix the database configuration before inviting users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production next steps
&lt;/h2&gt;

&lt;p&gt;The CLI workflow makes the Azure relationships visible. Before using it for a shared production environment, codify the deployment as IaC and add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure Key Vault references and a secret-rotation process.&lt;/li&gt;
&lt;li&gt;Microsoft Entra ID, a custom domain, and restricted ingress or a WAF.&lt;/li&gt;
&lt;li&gt;PostgreSQL backups, a restore test, and a production-appropriate compute tier. Consider &lt;code&gt;verify-full&lt;/code&gt; for the Grafana-to-PostgreSQL TLS connection after validating the container's CA trust.&lt;/li&gt;
&lt;li&gt;Diagnostics and alerts for revision failures, database capacity, and authentication issues.&lt;/li&gt;
&lt;li&gt;Versioned Grafana provisioning, plugin governance, and tested image upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Clean up
&lt;/h2&gt;

&lt;p&gt;For a disposable lab, delete the resource group after confirming it contains no shared resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deletion stops the ongoing costs for resources in that group. Check your subscription afterward for any separately scoped diagnostics, backups, or retained resources you intentionally created.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/" rel="noopener noreferrer"&gt;Grafana configuration documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/grafana/latest/setup-grafana/installation/docker/" rel="noopener noreferrer"&gt;Run Grafana in Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/container-apps/vnet-custom" rel="noopener noreferrer"&gt;Azure Container Apps virtual network integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/container-apps/manage-secrets" rel="noopener noreferrer"&gt;Azure Container Apps secrets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/container-apps/managed-identity-image-pull" rel="noopener noreferrer"&gt;Managed identity image pulls for Container Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/postgresql/network/concepts-networking-private" rel="noopener noreferrer"&gt;Azure Database for PostgreSQL private access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/container-registry/container-registry-rbac-built-in-roles-overview" rel="noopener noreferrer"&gt;ACR roles and ABAC repository permissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>cli</category>
      <category>grafana</category>
    </item>
    <item>
      <title>I Used Codex to Clean Up 77,000 Emails</title>
      <dc:creator>Khanh Nguyen</dc:creator>
      <pubDate>Wed, 22 Jul 2026 05:30:03 +0000</pubDate>
      <link>https://dev.to/khanh_nguyen_04b1205f5a91/i-used-codex-to-clean-up-77000-emails-21j</link>
      <guid>https://dev.to/khanh_nguyen_04b1205f5a91/i-used-codex-to-clean-up-77000-emails-21j</guid>
      <description>&lt;p&gt;A few days ago, I needed to find an old email in a Gmail account I had barely touched for years.&lt;/p&gt;

&lt;p&gt;Opening the inbox was overwhelming.&lt;/p&gt;

&lt;p&gt;It contained &lt;strong&gt;77,502 emails&lt;/strong&gt;, including &lt;strong&gt;72,356 unread messages&lt;/strong&gt;. Important conversations were buried beneath newsletters, promotions, job alerts, receipts, and automated notifications.&lt;/p&gt;

&lt;p&gt;Manually reviewing tens of thousands of emails was not realistic, and deleting everything was not an option.&lt;/p&gt;

&lt;p&gt;Instead, I used &lt;strong&gt;Codex with a connected Gmail workflow&lt;/strong&gt; to analyze the mailbox, design a safer organization system, and clean up the inbox in controlled stages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the problem
&lt;/h2&gt;

&lt;p&gt;Before changing anything, I asked Codex to analyze the mailbox and identify where most of the volume was coming from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F76lx0iuxvsr7g5c760ed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F76lx0iuxvsr7g5c760ed.png" alt="Understanding the problem" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The analysis revealed that more than &lt;strong&gt;92% of the mailbox consisted of updates and promotions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That changed how I approached the cleanup.&lt;/p&gt;

&lt;p&gt;The real problem was not simply the number of emails. It was that high-priority conversations and low-priority notifications were competing for the same space.&lt;/p&gt;

&lt;p&gt;The inbox needed structure before it needed deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  The system behind it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvaovhh2dkbak3p6wq7qx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvaovhh2dkbak3p6wq7qx.png" alt="Workflow" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The prompts I used
&lt;/h3&gt;

&lt;p&gt;The workflow was driven by a small set of prompts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How many emails are in my mailbox?&lt;/p&gt;

&lt;p&gt;Analyze my mailbox and suggest how to organize it using labels, rules, and a cleanup schedule.&lt;/p&gt;

&lt;p&gt;Start with labels and archive first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most important instruction was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Start with labels and archive first.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That constraint kept the workflow controlled and reversible.&lt;/p&gt;

&lt;p&gt;Archiving removed messages from the inbox without deleting them, while labels made it easier to review what had been classified before taking stronger action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules and label structure
&lt;/h3&gt;

&lt;p&gt;I used a few simple rules based on Codex’s recommendations and my own preferences to reduce the risk of removing something important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;strong&gt;Finance&lt;/strong&gt; and &lt;strong&gt;Security&lt;/strong&gt; emails visible&lt;/li&gt;
&lt;li&gt;Archive newsletters, job alerts, and routine notifications&lt;/li&gt;
&lt;li&gt;Exclude &lt;strong&gt;starred&lt;/strong&gt; and &lt;strong&gt;important&lt;/strong&gt; messages&lt;/li&gt;
&lt;li&gt;Preserve the original &lt;strong&gt;read and unread state&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Keep cleanup actions &lt;strong&gt;reversible&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of organizing messages only by sender, I grouped them according to what I might need to do next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Action/Finance&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Action/Security&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Career/Job Alerts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reading/Tech&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cleanup/30 Days&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made the labels more useful.&lt;/p&gt;

&lt;p&gt;A sender-based label tells me where an email came from. An action-based label tells me why the email still matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;After applying the workflow, the inbox became significantly smaller and easier to navigate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8ei6m0th8mvzn2t1mj1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8ei6m0th8mvzn2t1mj1.png" alt="Results" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The inbox was reduced from &lt;strong&gt;77,502 to 28,745 emails&lt;/strong&gt;, a &lt;strong&gt;62.7% reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During the cleanup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;48,296 emails were archived&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;461 emails were moved to Trash&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Read and unread states were preserved&lt;/li&gt;
&lt;li&gt;Finance and security emails remained visible&lt;/li&gt;
&lt;li&gt;No permanent bulk deletion was performed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was not to erase years of email history.&lt;/p&gt;

&lt;p&gt;It was to remove low-value messages from the inbox while keeping useful information searchable and recoverable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;The biggest improvement was not simply archiving &lt;strong&gt;48,296 emails&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It was turning an abandoned inbox into a system where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Important messages remain visible&lt;/li&gt;
&lt;li&gt;Useful emails remain searchable&lt;/li&gt;
&lt;li&gt;Routine notifications leave the inbox&lt;/li&gt;
&lt;li&gt;Low-value messages become easier to review&lt;/li&gt;
&lt;li&gt;Future cleanup becomes more manageable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Codex helped handle the scale&lt;/strong&gt;, but the workflow succeeded because the prompts were clear, the actions were staged, and the important decisions remained reversible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was created with AI assistance&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codex</category>
      <category>automation</category>
      <category>emailmanagement</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Moved My n8n Instance from Render to Azure</title>
      <dc:creator>Khanh Nguyen</dc:creator>
      <pubDate>Wed, 10 Jun 2026 04:01:55 +0000</pubDate>
      <link>https://dev.to/khanh_nguyen_04b1205f5a91/i-moved-my-n8n-instance-from-render-to-azure-1laa</link>
      <guid>https://dev.to/khanh_nguyen_04b1205f5a91/i-moved-my-n8n-instance-from-render-to-azure-1laa</guid>
      <description>&lt;p&gt;My n8n instance on Render worked perfectly.&lt;/p&gt;

&lt;p&gt;It was fast, simple, fully managed, and cost-effectiveness. And I barely had to think about infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlrmk0kxpx7bsvs0zz90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlrmk0kxpx7bsvs0zz90.png" alt=" " width="677" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  So why move it?
&lt;/h3&gt;

&lt;p&gt;Because I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More control over my environment&lt;/li&gt;
&lt;li&gt;Centralized cloud billing in Azure&lt;/li&gt;
&lt;li&gt;Hands-on experience with Infrastructure as Code (IaC), GitHub Actions, and cloud security&lt;/li&gt;
&lt;li&gt;A real-world project instead of another tutorial&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post shares what I built and what I learned along the way.&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/TQKNG/lab-n8n-azure.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Check out README file for full guide&lt;/em&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Wanted to Build
&lt;/h3&gt;

&lt;p&gt;I wanted a fully reproducible deployment where I could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy everything from GitHub Actions&lt;/li&gt;
&lt;li&gt;Authenticate to Azure without long-lived secrets (OIDC)&lt;/li&gt;
&lt;li&gt;Run n8n securely behind HTTPS&lt;/li&gt;
&lt;li&gt;Add identity + edge protection layers&lt;/li&gt;
&lt;li&gt;Tear everything down when not needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think: personal SaaS-grade infrastructure, but lightweight&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;This setup adds two important layers compared to a basic VM deployment:&lt;/p&gt;

&lt;p&gt;Cloudflare as the edge layer&lt;br&gt;
Azure AD (Entra ID) as the identity gate&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xmyyn6rtot9sldprler.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xmyyn6rtot9sldprler.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What Gets Deployed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Compute&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04 LTS VM (Azure)&lt;/li&gt;
&lt;li&gt;Trusted Launch enabled&lt;/li&gt;
&lt;li&gt;SSH key-based access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Networking&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Virtual Network (isolated subnet)&lt;/li&gt;
&lt;li&gt;Network Security Group (restricted ports)&lt;/li&gt;
&lt;li&gt;Public IP behind controlled ingress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n (Docker container)&lt;/li&gt;
&lt;li&gt;Caddy reverse proxy (HTTPS + routing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Edge &amp;amp; Identity&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare (DNS + protection layer)&lt;/li&gt;
&lt;li&gt;Azure AD (Entra ID authentication gate) 
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GitHub Actions Pipeline
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;To trace logs, I use below manually triggered workflows. These allows full lifecycle control from GitHub. Feel free to customize it based on your own workflow setup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Setup → initialize repo variables&lt;/li&gt;
&lt;li&gt;OIDC bootstrap → configure Azure trust&lt;/li&gt;
&lt;li&gt;Verify → test authentication&lt;/li&gt;
&lt;li&gt;Deploy → run Bicep infrastructure&lt;/li&gt;
&lt;li&gt;Start → validate VM + n8n endpoint&lt;/li&gt;
&lt;li&gt;Teardown → destroy resources safely
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Security Layers
&lt;/h3&gt;

&lt;p&gt;This architecture is intentionally layered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare → edge protection + TLS&lt;/li&gt;
&lt;li&gt;Azure AD → identity verification before access&lt;/li&gt;
&lt;li&gt;NSG → network-level filtering&lt;/li&gt;
&lt;li&gt;Caddy → HTTPS termination + reverse proxy&lt;/li&gt;
&lt;li&gt;Docker → container isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a simple zero-trust-inspired flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Learned
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Azure OIDC is a game changer. No more storing long-lived secrets in GitHub.&lt;/li&gt;
&lt;li&gt;Bicep is actually pleasant. Much easier than ARM templates and still powerful.&lt;/li&gt;
&lt;li&gt;Cloudflare simplifies edge security. It reduces direct exposure of Azure resources.&lt;/li&gt;
&lt;li&gt;Infrastructure as Code changes everything. Even a small project becomes reproducible and disposable.

&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Next Step
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add Azure Key Vault for secrets&lt;/li&gt;
&lt;li&gt;Add automated backups for n8n workflows&lt;/li&gt;
&lt;li&gt;Move monitoring to Azure Monitor dashboards&lt;/li&gt;
&lt;li&gt;Restrict SSH via IP allowlist or JIT access&lt;/li&gt;
&lt;li&gt;Evaluate Azure Container Apps instead of VM&lt;/li&gt;
&lt;li&gt;Add cost automation (auto shutdown dev environment)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a href="https://github.com/TQKNG/lab-n8n-azure" rel="noopener noreferrer"&gt;https://github.com/TQKNG/lab-n8n-azure&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;n8n Documentation: &lt;a href="https://docs.n8n.io" rel="noopener noreferrer"&gt;https://docs.n8n.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudfare Developer Docs: &lt;a href="https://developers.cloudflare.com/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Azure&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bicep Docs: &lt;a href="https://learn.microsoft.com/azure/azure-resource-manager/bicep" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/azure-resource-manager/bicep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Azure OIDC with GitHub Actions: &lt;a href="https://learn.microsoft.com/azure/developer/github/connect-from-azure-openid-connect" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/developer/github/connect-from-azure-openid-connect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Azure Well-Architected Framework: &lt;a href="https://learn.microsoft.com/azure/architecture/framework" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/architecture/framework&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CI/CD &amp;amp; Infra&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions Docs: &lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;https://docs.github.com/actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Docs: &lt;a href="https://docs.docker.com" rel="noopener noreferrer"&gt;https://docs.docker.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Caddy Server Docs: &lt;a href="https://caddyserver.com/docs" rel="noopener noreferrer"&gt;https://caddyserver.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>bicep</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
