{"id":1684,"date":"2025-03-06T17:27:58","date_gmt":"2025-03-06T17:27:58","guid":{"rendered":"https:\/\/editjournal.redakt.eu\/faxmodem\/?p=1684"},"modified":"2025-05-05T14:32:14","modified_gmt":"2025-05-05T13:32:14","slug":"deploy-python-bot-azure-web-app-free","status":"publish","type":"post","link":"https:\/\/editjournal.redakt.eu\/faxmodem\/blog\/development\/cloud-serverless\/deploy-python-bot-azure-web-app-free\/","title":{"rendered":"Deploying a Python bot to Azure Web App Service (Free Tier)"},"content":{"rendered":"<h1>Run a Python bot for free on Azure Web App Service<\/h1>\n<p>Pre-requisite: have a bot. You can use <a href=\"https:\/\/github.com\/mehov\/bot-clock\">mehov\/bot-clock<\/a> as an example. It works with Telegram, so you will need <code>TELEGRAM_BOT_TOKEN<\/code> and <code>TELEGRAM_CHAT_ID <\/code>- see the bot's readme for more info.<\/p>\n<h2>Create a new Web App<\/h2>\n<p>Go to <a href=\"https:\/\/portal.azure.com\/\">Azure Portal<\/a> \u2192 App Services and create a new <em>Web App<\/em>:<\/p>\n<ul>\n<li><em>Publish<\/em>: <strong>Code<\/strong><\/li>\n<li><em>Runtime stack<\/em>: <strong>Python<\/strong><\/li>\n<li><em>Operating System<\/em>: <strong>Linux<\/strong><\/li>\n<li>Database, Deployment, Monitor + secure: disable or reject everything<\/li>\n<li>Networking \u2192 <em>Enable public access<\/em>: <strong>On<\/strong><\/li>\n<\/ul>\n<p>Disable Deployment for now - you will configure it later after the app has been created.<\/p>\n<h2>Configure environment<\/h2>\n<p>Go to Settings \u2192 Environment Variables<\/p>\n<ul>\n<li>set <code>TELEGRAM_BOT_TOKEN<\/code> and <code>TELEGRAM_CHAT_ID<\/code>; see the bot's readme for more info<\/li>\n<\/ul>\n<h2>Configure Startup: gunicorn<\/h2>\n<p>Under Settings \u2192 Configuration \u2192 General settings \u2192 <em>Startup Command<\/em>:<\/p>\n<pre><code>chmod +x $APP_PATH\/setup.sh && $APP_PATH\/setup.sh \"$WEBSITE_HOSTNAME\" && gunicorn --bind=$HOST:$PORT app:app<\/code><\/pre>\n<p>The above runs <a href=\"https:\/\/github.com\/mehov\/bot-clock\/blob\/master\/setup.sh\"><code>setup.sh<\/code><\/a> on startup. The script ensures the CRON command is set up. It then starts the Azure app using <code>gunicorn<\/code> - more info on that below under Gotchas. <code>$WEBSITE_HOSTNAME<\/code>, <code>$HOST<\/code> and <code>$PORT<\/code> are retrieved from the environment.<\/p>\n<h2>Configure Startup: manual<\/h2>\n<p>Sometimes you may want to handle gunicorn yourself. For example, if you want to create and run an instance of <code>Application.builder()<\/code> from <code>telegram.ext<\/code> and have it share the event loop with Flask. For that you will need to define the <code>uvicorn.Server<\/code> yourself and it would look like this:<\/p>\n<pre><code>import asyncio\nimport telegram\nimport uvicorn\nfrom asgiref.wsgi import WsgiToAsgi\nfrom flask import Flask\nfrom telegram.ext import (\n    Application,\n)\n\nflask_app = Flask(__name__)\ntelegram_app = Application.builder().token(&#039;TELEGRAM_BOT_TOKEN&#039;).updater(None).build()\n\nasync def main():\n    webserver = uvicorn.Server(\n        config=uvicorn.Config(\n            app=WsgiToAsgi(flask_app),\n            port=8000,\n            use_colors=False,\n            host=&#039;0.0.0.0&#039;,\n        )\n    )\n    async with telegram_app:\n        await telegram_app.start()\n        await webserver.serve()\n        await telegram_app.stop()\n\nif __name__ == &#039;__main__&#039;:\n    asyncio.run(main())<\/code><\/pre>\n<p>Under Settings \u2192 Configuration \u2192 General settings \u2192 <em>Startup Command<\/em>:<\/p>\n<pre><code>chmod +x $APP_PATH\/setup.sh && $APP_PATH\/setup.sh \"$WEBSITE_HOSTNAME\" && HOME=\"\/home\/site\/wwwroot\" python3 app.py<\/code><\/pre>\n<p>Similarly to the gunicorn startup command, this one begins with setting up cron, and then it just starts the script directly (making sure it has Azure's persistent location as <code>$HOME<\/code>)<\/p>\n<h2>Configure deployment<\/h2>\n<p>Still under Settings \u2192 Configuration \u2192 General settings:<\/p>\n<ul>\n<li>set <em>SCM Basic Auth Publishing Credentials<\/em> to <strong>On<\/strong> - when disabled, Github actions that rely on HTTP basic auth <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/app-service\/configure-basic-auth-disable?tabs=portal#deploy-without-basic-authentication\">won't work<\/a><\/li>\n<li><em>(optional)<\/em> enable or disable FTP and SSH to your preference<\/li>\n<\/ul>\n<p>Then go to App Services \u2192 <em>Your New App<\/em> \u2192 Deployment \u2192 Deployment Center. Open Settings.<\/p>\n<ul>\n<li>Source: <strong>External Git<\/strong> (instead of choosing Github - this way you don't have to use Github Actions\/Workflows, Azure picks up the commits by itself)<\/li>\n<li>Build Provider: <strong>App Service Build Service<\/strong><\/li>\n<li>Repository: <a href=\"https:\/\/github.com\/mehov\/bot-clock.git\">https:\/\/github.com\/mehov\/bot-clock.git<\/a><\/li>\n<li>Branch: <strong>master<\/strong><\/li>\n<\/ul>\n<p>If you want Azure to have access to your private repositories, authorise Azure to your Github account.<\/p>\n<h2>Automatic deployment<\/h2>\n<p>If you want your app to be deployed to Azure automatically on each commit, you will have to set up a webhook in your repo settings.<\/p>\n<ol>\n<li>Get the secrets from Azure. Go to <em>Deployment<\/em> \u2192 <em>Deployment Center<\/em> \u2192 <em>Manage Publish Profile<\/em>. In the pop-up click <em>Download publish profile<\/em>. In the XML file that downloads, in the entry where it says <em>profileName<\/em>=&quot;Web Deploy&quot; and <em>publishMethod<\/em>=&quot;MSDeploy&quot;, you're looking for the following values:\n<ul>\n<li><strong>publishUrl<\/strong>: <code>{your-app}.scm.azurewebsites.net:443<\/code><\/li>\n<li><strong>userName<\/strong>: <code>${your-app}<\/code>, <code>$<\/code> is important<\/li>\n<li><strong>userPWD<\/strong>: <code>{BigRandomPassword}<\/code><\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Go to <code>https:\/\/github.com\/{you}\/{repo}\/settings\/hooks<\/code> and create a new webhook that looks like this:<\/p>\n<pre><code> https:\/\/{userName}:{userPWD}@{publishUrl}\/deploy<\/code><\/pre>\n<p>More info:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/app-service\/deploy-continuous-deployment?tabs=others%2Cappservice#configure-the-deployment-source\">https:\/\/learn.microsoft.com\/en-us\/azure\/app-service\/deploy-continuous-deployment?tabs=others%2Cappservice#configure-the-deployment-source<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/projectkudu\/kudu\/wiki\/Continuous-deployment#setting-up-continuous-deployment-using-manual-steps\">https:\/\/github.com\/projectkudu\/kudu\/wiki\/Continuous-deployment#setting-up-continuous-deployment-using-manual-steps<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<h2>Gotchas<\/h2>\n<h3>Because <em>Startup Command<\/em> is not empty Azure won't automatically start the app<\/h3>\n<p>When you create an app, <em>Startup Command<\/em> is normally empty, and so Azure automatically detects the web app environment and then autostarts the app. If a WSGI-compatible app, e.g. based on Flask, is detected, Azure uses WSGI servers like Gunicorn to handle concurrent requests and manage threading, as Flask\u2019s built-in server (<code>app.run()<\/code>) is not suitable for production environments.<\/p>\n<p>But if you override <em>Startup Command<\/em>, you must manually start your web server after any setup scripts.<\/p>\n<p>If you set <em>Startup Command<\/em> that only runs some housekeeping script and exits, Azure won\u2019t autostart the app because it assumes you\u2019re handling it yourself. In other words, if your custom <em>Startup Command exits<\/em>, Azure stops because there\u2019s no running process.<\/p>\n<h3>Autostart? Azure looks for <code>app.py<\/code> with <code>app<\/code> variable in it<\/h3>\n<p>If <em>Startup Command<\/em> is empty and Azure Web App Service detects your app as Python and Flask powered, the default launch command is <code>gunicorn -b 0.0.0.0:8000 app:app<\/code>, where <code>app:app<\/code> is <code>MODULE_NAME:VARIABLE_NAME<\/code>:<\/p>\n<ul>\n<li><code>MODULE_NAME<\/code>: The Python module (file) that contains the application, <code>app.py<\/code> by default.<\/li>\n<li><code>VARIABLE_NAME<\/code>: The WSGI application instance (a Python variable inside the module), <code>app<\/code> by default.<\/li>\n<\/ul>\n<p>For the above launch command to work, your main script needs to be <code>app.py<\/code> and the WSGI-compatible instance in it needs to be in the variable named <code>app<\/code>. If you import it and it has a different name where you import it from, you can alias it:<\/p>\n<pre><code>from src.Http import http as app<\/code><\/pre>\n<p>Here's detailed info on the Azure logic: <a href=\"https:\/\/azureossd.github.io\/2023\/01\/30\/Troubleshooting-&#039;failed-to-find-attribute&#039;-errors-on-Python-Linux-App-Services\/\">azureossd.github.io\/2023\/01\/30\/Troubleshooting-'failed-to-find-attribute'-errors-on-Python-Linux-App-Services<\/a><\/p>\n<h3>You don't have to call <code>app.run()<\/code> in your python script<\/h3>\n<p>Azure will handle it automatically.<\/p>\n<h2>More info<\/h2>\n<ul>\n<li><a href=\"https:\/\/azureossd.github.io\/2023\/02\/15\/Whats-the-difference-between-PORT-and-WEBSITES_PORT\/index.html\">https:\/\/azureossd.github.io\/2023\/02\/15\/Whats-the-difference-between-PORT-and-WEBSITES_PORT\/index.html<\/a><\/li>\n<li><a href=\"https:\/\/azureossd.github.io\/2023\/04\/13\/Using-custom-startup-commands-with-Web-App-for-Containers\/\">https:\/\/azureossd.github.io\/2023\/04\/13\/Using-custom-startup-commands-with-Web-App-for-Containers\/<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Run a Python bot for free on Azure Web App Service Pre-requisite: have a bot. You can use mehov\/bot-clock as an example. It works with Telegram, so you will need TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID &#8211; see the bot&#8217;s readme for more info. Create a new Web App Go to Azure Portal \u2192 App Services and create&hellip; <a class=\"more-link\" href=\"https:\/\/editjournal.redakt.eu\/faxmodem\/blog\/development\/cloud-serverless\/deploy-python-bot-azure-web-app-free\/\">Continue reading <span class=\"screen-reader-text\">Deploying a Python bot to Azure Web App Service (Free Tier)<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[778],"tags":[1253,824,935],"class_list":["post-1684","post","type-post","status-publish","format-standard","hentry","category-cloud-serverless","tag-bot","tag-microsoft-azure","tag-python","entry"],"_links":{"self":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts\/1684","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/comments?post=1684"}],"version-history":[{"count":9,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts\/1684\/revisions"}],"predecessor-version":[{"id":1699,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/posts\/1684\/revisions\/1699"}],"wp:attachment":[{"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/media?parent=1684"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/categories?post=1684"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/editjournal.redakt.eu\/faxmodem\/wp-json\/wp\/v2\/tags?post=1684"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}