{"id":71638,"date":"2025-05-06T10:38:06","date_gmt":"2025-05-06T05:08:06","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=71638"},"modified":"2026-04-22T11:23:22","modified_gmt":"2026-04-22T05:53:22","slug":"setting-up-elastic-apm-with-java-applications-on-azure","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/setting-up-elastic-apm-with-java-applications-on-azure\/","title":{"rendered":"Integrating Elastic APM with a Java Spring Boot App in Docker on Azure"},"content":{"rendered":"<h1><strong>Introduction<\/strong><\/h1>\n<p>We recently needed visibility into what our Java services were actually doing in production \u2014 response times, slow queries, errors, that sort of thing. We landed on Elastic APM. Here&#8217;s exactly how we set it up, including some decisions we made around multi-environment support that saved us a lot of headache later.<\/p>\n<p>The setup covers:<\/p>\n<ul>\n<li>Installing and configuring the APM Server<\/li>\n<li>Attaching the Elastic APM Java Agent to a Spring Boot application<\/li>\n<li>Using a single Dockerfile and an init script across environments<\/li>\n<li>Parameterizing the configuration so UAT and Production send data to their respective APM servers<\/li>\n<\/ul>\n<p>This approach works well when you want consistency across environments without duplicating Docker images or startup logic.<\/p>\n<p><strong>Step 1:\u00a0Find the APM setup in Kibana<br \/>\n<\/strong>Log into Kibana, go to\u00a0Observability \u2192 APM, and follow the on-screen instructions. It&#8217;ll ask for your application type and OS. This gets you the right install path without guessing.<\/p>\n<div id=\"attachment_71632\" style=\"width: 635px\" class=\"wp-caption aligncenter\"><img aria-describedby=\"caption-attachment-71632\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-71632 size-large\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver-1024x576.png\" alt=\"APM server\" width=\"625\" height=\"352\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver-1024x576.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver-300x169.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver-768x432.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver-1536x864.png 1536w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver-624x351.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentserver.png 1600w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-71632\" class=\"wp-caption-text\">server<\/p><\/div>\n<p><strong>Step 2: \u00a0Install APM Server on the Elasticsearch VM<\/strong><\/p>\n<p>On the VM running Elasticsearch, install and start the APM Server:<\/p>\n<pre>sudo apt install apm-server\r\nsudo systemctl enable apm-server\r\nsudo systemctl start apm-server<\/pre>\n<p>Once it&#8217;s running, the server listens on port\u00a08200. At this stage we&#8217;re just confirming the backend is up and ready to receive data before touching the application.<\/p>\n<p><strong>Step 3: Get the APM Java agent and store it in Blob Storage<\/strong><\/p>\n<p>Download the agent JAR from Maven:<\/p>\n<pre>wget https:\/\/search.maven.org\/remotecontent?filepath=co\/elastic\/apm\/elastic-apm-agent\/1.52.1\/elastic-apm-agent-1.52.1.jar<\/pre>\n<div id=\"attachment_71634\" style=\"width: 635px\" class=\"wp-caption aligncenter\"><img aria-describedby=\"caption-attachment-71634\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-71634 size-large\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload-1024x555.png\" alt=\"jar download\" width=\"625\" height=\"339\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload-1024x555.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload-300x163.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload-768x416.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload-1536x832.png 1536w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload-624x338.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agentdownload.png 1600w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-71634\" class=\"wp-caption-text\">apm jar<\/p><\/div>\n<p>Rather than baking this into the Docker image, we pushed it to Azure Blob Storage. The reason: when Elastic releases a new agent version, we can swap it out without touching the image or triggering a full rebuild. Same logic applies if you ever need to roll back \u2014 just point to the old JAR.<\/p>\n<p><strong>Step 4: Pull the agent at startup and attach it to the app<\/strong><\/p>\n<p>Inside\u00a0init.sh\u00a0(the container&#8217;s startup script), we download the agent from Blob Storage using\u00a0azcopy, then pass it as a\u00a0-javaagent\u00a0flag when starting the application:<\/p>\n<pre>azcopy cp \"https:\/\/&lt;storage-account&gt;.blob.core.windows.net\/apmkibana\/elastic-apm-agent-1.52.1.jar?&lt;SASTOKEN&gt;\" \"\/usr\/local\/apm\"<\/pre>\n<pre>java -javaagent:\/usr\/local\/apm\/elastic-apm-agent-1.52.1.jar \\\r\n\u00a0\u00a0\u00a0\u00a0-Delastic.apm.service_name=\"${apm_service_name}\" \\\r\n\u00a0\u00a0\u00a0\u00a0-Delastic.apm.server_urls=\"${hostname}\" \\\r\n\u00a0\u00a0\u00a0\u00a0-Delastic.apm.secret_token= \\\r\n\u00a0\u00a0\u00a0\u00a0-Delastic.apm.application_packages=org.example \\\r\n\u00a0\u00a0\u00a0\u00a0-jar \/app\/application.jar \\\r\n\u00a0\u00a0\u00a0\u00a0--spring.config.location=...<\/pre>\n<p>The service name and APM server URL come in as environment variables \u2014 no hardcoded values anywhere. This means the same startup script handles UAT and production without any changes.<\/p>\n<div id=\"attachment_71633\" style=\"width: 635px\" class=\"wp-caption aligncenter\"><img aria-describedby=\"caption-attachment-71633\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-71633 size-large\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup-1024x576.png\" alt=\"add config\" width=\"625\" height=\"352\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup-1024x576.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup-300x169.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup-768x432.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup-1536x864.png 1536w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup-624x351.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/agent-setup.png 1600w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-71633\" class=\"wp-caption-text\">agent<\/p><\/div>\n<p><strong>Step 5: The Dockerfile stays generic<\/strong><\/p>\n<p>We deliberately kept the Dockerfile environment-agnostic. All the environment-specific behaviour lives in runtime config, not the image:<\/p>\n<pre>FROM xyz.azurecr.io\/java_baseimage:latest\r\nARG artifact_version\r\nLABEL artifact_version=$artifact_version\r\nWORKDIR \/app\r\nCOPY init.sh \/init.sh\r\nRUN chmod 500 \/init.sh\r\nRUN mkdir -p \/tmp\/images \/opt\/conf \/opt\/tmp \/usr\/local\/apm\r\nCOPY build\/libs\/*.jar application.jar\r\nENTRYPOINT \/init.sh<\/pre>\n<p>One image, all environments. Keeps things simple and avoids drift between UAT and production builds.<\/p>\n<p><strong>Step 6: The full init.sh<\/strong><\/p>\n<p>Here&#8217;s the startup script in full. A few things worth noting: JVM tuning flags are set here (G1GC, heap bounds, GC logging), the APM agent is downloaded fresh on each container start, and application config files are pulled from Blob Storage too:<\/p>\n<pre>#!\/bin\/bash\r\nset -xe\r\nJAVA=$(which java)\r\n# Download APM agent\r\nazcopy cp \"https:\/\/xyz.blob.core.windows.net\/apmkibana\/elastic-apm-agent-1.52.1.jar?&lt;SASTOKEN&gt;\" \"\/usr\/local\/apm\"\r\n# Download config\r\nbucket=${CONFIG_BUCKET}\r\nazcopy cp \"${bucket}\/${namespace}\/${CONFIG}\/core\/resources\/application-${filename}-common.properties${BUCKET_TOKEN}\" \"\/opt\/conf\/core\/application-${filename}-common.properties\"\r\nazcopy cp \"${bucket}\/${namespace}\/${CONFIG}\/${APP_TYPE}\/resources\/application-${filename}.properties${BUCKET_TOKEN}\" \"\/opt\/conf\/${APP_TYPE}\/application-${filename}.properties\"\r\nchmod -R 777 \/app \/opt\/conf \/opt\/tmp\r\n$JAVA -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=70 \\\r\n\u00a0\u00a0-Xms500m -Xmx1000m -XX:MaxGCPauseMillis=200 \\\r\n\u00a0\u00a0-XX:+UseG1GC -XX:+UseStringDeduplication -Xlog:gc*:\/opt\/tmp\/myapp-gc.log \\\r\n\u00a0\u00a0-verbose:gc \\\r\n\u00a0\u00a0-Djava.security.egd=file:\/dev\/.\/urandom \\\r\n\u00a0\u00a0-javaagent:\/usr\/local\/apm\/elastic-apm-agent-1.52.1.jar \\\r\n\u00a0\u00a0-Delastic.apm.service_name=\"${apm_service_name}\" \\\r\n\u00a0\u00a0-Delastic.apm.server_urls=\"${hostname}\" \\\r\n\u00a0\u00a0-Delastic.apm.secret_token= \\\r\n\u00a0\u00a0-Delastic.apm.application_packages=org.example \\\r\n\u00a0\u00a0-jar \/app\/application.jar \\\r\n\u00a0\u00a0--spring.config.location=optional:\/opt\/conf\/core\/application-${filename}-common.properties,optional:\/opt\/conf\/${APP_TYPE}\/application-${filename}.properties<\/pre>\n<p><strong>Environment-based APM Configuration<\/strong><br \/>\nTo support multiple environments (like UAT and Production), we\u2019ve parameterized the APM server URL and service name inside init.sh. This ensures the correct data is sent to the appropriate APM Server based on the environment variables.<\/p>\n<p><strong>Final Step: Verify APM Data<\/strong><br \/>\nGo back to Kibana &gt; Observability &gt; APM and check if your application\u2019s telemetry is being displayed. If everything is configured correctly, you should see:<\/p>\n<ol>\n<li>Service names<\/li>\n<li>Request traces<\/li>\n<li>Performance metrics<\/li>\n<li>Errors (if any)<\/li>\n<\/ol>\n<div id=\"attachment_71631\" style=\"width: 635px\" class=\"wp-caption aligncenter\"><img aria-describedby=\"caption-attachment-71631\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-71631 size-large\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/04\/final-1024x292.png\" alt=\"Fianl data\" width=\"625\" height=\"178\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/04\/final-1024x292.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/final-300x86.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/final-768x219.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/final-1536x438.png 1536w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/final-624x178.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/04\/final.png 1600w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-71631\" class=\"wp-caption-text\">apm dashboard<\/p><\/div>\n<h2><strong>Note on multi-environment setup:<\/strong><\/h2>\n<pre> UAT and production each have their own APM Server. The ${hostname} and ${apm_service_name} variables are set differently per environment in the container's runtime config \u2014 the script itself doesn't change. This was the key thing we wanted to avoid: duplicating startup logic or maintaining separate images per environment.<\/pre>\n<h2><strong>Verifying it works<\/strong><\/h2>\n<p>Once the container is running, go back to Kibana \u2192 Observability \u2192 APM. If the agent connected successfully you&#8217;ll start seeing your service name appear, along with request traces, response time distributions, and any errors. Give it a minute after the first request \u2014 it&#8217;s not instant.<\/p>\n<p>The things we found most useful straight away: slow database queries showing up in traces, and being able to correlate a spike in error rate to a specific deployment. That alone made the setup worth it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction We recently needed visibility into what our Java services were actually doing in production \u2014 response times, slow queries, errors, that sort of thing. We landed on Elastic APM. Here&#8217;s exactly how we set it up, including some decisions we made around multi-environment support that saved us a lot of headache later. The setup [&hellip;]<\/p>\n","protected":false},"author":1744,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":12},"categories":[2348],"tags":[6158,7274,1524,4844,4446],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/71638"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/1744"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=71638"}],"version-history":[{"count":5,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/71638\/revisions"}],"predecessor-version":[{"id":79367,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/71638\/revisions\/79367"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=71638"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=71638"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=71638"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}